aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGELOG.md2
-rw-r--r--package.json4
-rw-r--r--src/commands/play.ts212
-rw-r--r--src/commands/queue.ts60
-rw-r--r--src/commands/skip.ts16
-rw-r--r--src/commands/unskip.ts9
-rw-r--r--src/services/get-songs.ts20
-rw-r--r--src/services/natural-language-commands.ts6
-rw-r--r--src/services/player.ts2
-rw-r--r--src/utils/build-embed.ts127
-rw-r--r--src/utils/string.ts2
-rw-r--r--yarn.lock19
12 files changed, 303 insertions, 176 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index c48b67e..d868790 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
+### Changed
+- Queue embeds are now more detailed and appear when resuming playback. Thanks @bokherus!
## [0.4.0] - 2022-01-17
### Added
diff --git a/package.json b/package.json
index 801becb..1a36193 100644
--- a/package.json
+++ b/package.json
@@ -112,7 +112,7 @@
"spotify-web-api-node": "^5.0.2",
"xbytes": "^1.7.0",
"youtube.ts": "^0.2.5",
- "ytdl-core": "^4.9.2",
- "ytsr": "^3.5.3"
+ "ytdl-core": "^4.10.0",
+ "ytsr": "^3.6.0"
}
}
diff --git a/src/commands/play.ts b/src/commands/play.ts
index 784885e..16d16dd 100644
--- a/src/commands/play.ts
+++ b/src/commands/play.ts
@@ -12,6 +12,7 @@ import errorMsg from '../utils/error-msg.js';
import Command from '.';
import GetSongs from '../services/get-songs.js';
import {prisma} from '../utils/db.js';
+import {buildPlayingMessageEmbed} from '../utils/build-embed.js';
@injectable()
export default class implements Command {
@@ -57,143 +58,154 @@ export default class implements Command {
const res = new LoadingMessage(msg.channel as TextChannel);
await res.start();
- const player = this.playerManager.get(msg.guild!.id);
+ try {
+ const player = this.playerManager.get(msg.guild!.id);
- const wasPlayingSong = player.getCurrent() !== null;
+ const wasPlayingSong = player.getCurrent() !== null;
- if (args.length === 0) {
- if (player.status === STATUS.PLAYING) {
- await res.stop(errorMsg('already playing, give me a song name'));
- return;
- }
+ if (args.length === 0) {
+ if (player.status === STATUS.PLAYING) {
+ await res.stop(errorMsg('already playing, give me a song name'));
+ return;
+ }
- // Must be resuming play
- if (!wasPlayingSong) {
- await res.stop(errorMsg('nothing to play'));
- return;
- }
+ // Must be resuming play
+ if (!wasPlayingSong) {
+ await res.stop(errorMsg('nothing to play'));
+ return;
+ }
- await player.connect(targetVoiceChannel);
- await player.play();
+ await player.connect(targetVoiceChannel);
+ await player.play();
- await res.stop('the stop-and-go light is now green');
- return;
- }
+ await Promise.all([
+ res.stop('the stop-and-go light is now green'),
+ msg.channel.send({embeds: [buildPlayingMessageEmbed(player)]}),
+ ]);
- const addToFrontOfQueue = args[args.length - 1] === 'i' || args[args.length - 1] === 'immediate';
- const shuffleAdditions = args[args.length - 1] === 's' || args[args.length - 1] === 'shuffle';
+ return;
+ }
- let newSongs: Array<Except<QueuedSong, 'addedInChannelId'>> = [];
- let extraMsg = '';
+ const addToFrontOfQueue = args[args.length - 1] === 'i' || args[args.length - 1] === 'immediate';
+ const shuffleAdditions = args[args.length - 1] === 's' || args[args.length - 1] === 'shuffle';
- // Test if it's a complete URL
- try {
- const url = new URL(args[0]);
+ let newSongs: Array<Except<QueuedSong, 'addedInChannelId' | 'requestedBy'>> = [];
+ let extraMsg = '';
- const YOUTUBE_HOSTS = [
- 'www.youtube.com',
- 'youtu.be',
- 'youtube.com',
- 'music.youtube.com',
- 'www.music.youtube.com',
- ];
+ // Test if it's a complete URL
+ try {
+ const url = new URL(args[0]);
- if (YOUTUBE_HOSTS.includes(url.host)) {
+ const YOUTUBE_HOSTS = [
+ 'www.youtube.com',
+ 'youtu.be',
+ 'youtube.com',
+ 'music.youtube.com',
+ 'www.music.youtube.com',
+ ];
+
+ if (YOUTUBE_HOSTS.includes(url.host)) {
// YouTube source
- if (url.searchParams.get('list')) {
+ if (url.searchParams.get('list')) {
// YouTube playlist
- newSongs.push(...await this.getSongs.youtubePlaylist(url.searchParams.get('list')!));
- } else {
- // Single video
- const song = await this.getSongs.youtubeVideo(url.href);
-
- if (song) {
- newSongs.push(song);
+ newSongs.push(...await this.getSongs.youtubePlaylist(url.searchParams.get('list')!));
} else {
- await res.stop(errorMsg('that doesn\'t exist'));
- return;
+ // Single video
+ const song = await this.getSongs.youtubeVideo(url.href);
+
+ if (song) {
+ newSongs.push(song);
+ } else {
+ await res.stop(errorMsg('that doesn\'t exist'));
+ return;
+ }
}
- }
- } else if (url.protocol === 'spotify:' || url.host === 'open.spotify.com') {
- const [convertedSongs, nSongsNotFound, totalSongs] = await this.getSongs.spotifySource(args[0], playlistLimit);
+ } else if (url.protocol === 'spotify:' || url.host === 'open.spotify.com') {
+ const [convertedSongs, nSongsNotFound, totalSongs] = await this.getSongs.spotifySource(args[0], playlistLimit);
- if (totalSongs > playlistLimit) {
- extraMsg = `a random sample of ${playlistLimit} songs was taken`;
- }
+ if (totalSongs > playlistLimit) {
+ extraMsg = `a random sample of ${playlistLimit} songs was taken`;
+ }
- if (totalSongs > playlistLimit && nSongsNotFound !== 0) {
- extraMsg += ' and ';
- }
+ if (totalSongs > playlistLimit && nSongsNotFound !== 0) {
+ extraMsg += ' and ';
+ }
- if (nSongsNotFound !== 0) {
- if (nSongsNotFound === 1) {
- extraMsg += '1 song was not found';
- } else {
- extraMsg += `${nSongsNotFound.toString()} songs were not found`;
+ if (nSongsNotFound !== 0) {
+ if (nSongsNotFound === 1) {
+ extraMsg += '1 song was not found';
+ } else {
+ extraMsg += `${nSongsNotFound.toString()} songs were not found`;
+ }
}
- }
- newSongs.push(...convertedSongs);
- }
- } catch (_: unknown) {
+ newSongs.push(...convertedSongs);
+ }
+ } catch (_: unknown) {
// Not a URL, must search YouTube
- const query = addToFrontOfQueue ? args.slice(0, args.length - 1).join(' ') : args.join(' ');
+ const query = addToFrontOfQueue ? args.slice(0, args.length - 1).join(' ') : args.join(' ');
- const song = await this.getSongs.youtubeVideoSearch(query);
+ const song = await this.getSongs.youtubeVideoSearch(query);
- if (song) {
- newSongs.push(song);
- } else {
- await res.stop(errorMsg('that doesn\'t exist'));
+ if (song) {
+ newSongs.push(song);
+ } else {
+ await res.stop(errorMsg('that doesn\'t exist'));
+ return;
+ }
+ }
+
+ if (newSongs.length === 0) {
+ await res.stop(errorMsg('no songs found'));
return;
}
- }
- if (newSongs.length === 0) {
- await res.stop(errorMsg('no songs found'));
- return;
- }
+ if (shuffleAdditions) {
+ newSongs = shuffle(newSongs);
+ }
- if (shuffleAdditions) {
- newSongs = shuffle(newSongs);
- }
+ newSongs.forEach(song => {
+ player.add({...song, addedInChannelId: msg.channel.id, requestedBy: msg.author.id}, {immediate: addToFrontOfQueue});
+ });
- newSongs.forEach(song => {
- player.add({...song, addedInChannelId: msg.channel.id}, {immediate: addToFrontOfQueue});
- });
+ const firstSong = newSongs[0];
- const firstSong = newSongs[0];
+ let statusMsg = '';
- let statusMsg = '';
+ if (player.voiceConnection === null) {
+ await player.connect(targetVoiceChannel);
- if (player.voiceConnection === null) {
- await player.connect(targetVoiceChannel);
+ // Resume / start playback
+ await player.play();
- // Resume / start playback
- await player.play();
+ if (wasPlayingSong) {
+ statusMsg = 'resuming playback';
+ }
- if (wasPlayingSong) {
- statusMsg = 'resuming playback';
+ await msg.channel.send({embeds: [buildPlayingMessageEmbed(player)]});
}
- }
- // Build response message
- if (statusMsg !== '') {
- if (extraMsg === '') {
- extraMsg = statusMsg;
- } else {
- extraMsg = `${statusMsg}, ${extraMsg}`;
+ // Build response message
+ if (statusMsg !== '') {
+ if (extraMsg === '') {
+ extraMsg = statusMsg;
+ } else {
+ extraMsg = `${statusMsg}, ${extraMsg}`;
+ }
}
- }
- if (extraMsg !== '') {
- extraMsg = ` (${extraMsg})`;
- }
+ if (extraMsg !== '') {
+ extraMsg = ` (${extraMsg})`;
+ }
- if (newSongs.length === 1) {
- await res.stop(`u betcha, **${firstSong.title}** added to the${addToFrontOfQueue ? ' front of the' : ''} queue${extraMsg}`);
- } else {
- await res.stop(`u betcha, **${firstSong.title}** and ${newSongs.length - 1} other songs were added to the queue${extraMsg}`);
+ if (newSongs.length === 1) {
+ await res.stop(`u betcha, **${firstSong.title}** added to the${addToFrontOfQueue ? ' front of the' : ''} queue${extraMsg}`);
+ } else {
+ await res.stop(`u betcha, **${firstSong.title}** and ${newSongs.length - 1} other songs were added to the queue${extraMsg}`);
+ }
+ } catch (error) {
+ await res.stop();
+ throw error;
}
}
}
diff --git a/src/commands/queue.ts b/src/commands/queue.ts
index 58d6377..79285b1 100644
--- a/src/commands/queue.ts
+++ b/src/commands/queue.ts
@@ -1,15 +1,9 @@
-import {Message, MessageEmbed} from 'discord.js';
-import getYouTubeID from 'get-youtube-id';
+import {Message} from 'discord.js';
import {inject, injectable} from 'inversify';
import {TYPES} from '../types.js';
import PlayerManager from '../managers/player.js';
-import {STATUS} from '../services/player.js';
import Command from '.';
-import getProgressBar from '../utils/get-progress-bar.js';
-import errorMsg from '../utils/error-msg.js';
-import {prettyTime} from '../utils/time.js';
-
-const PAGE_SIZE = 10;
+import {buildQueueEmbed} from '../utils/build-embed.js';
@injectable()
export default class implements Command {
@@ -29,54 +23,8 @@ export default class implements Command {
public async execute(msg: Message, args: string []): Promise<void> {
const player = this.playerManager.get(msg.guild!.id);
- const currentlyPlaying = player.getCurrent();
-
- if (currentlyPlaying) {
- const queueSize = player.queueSize();
- const queuePage = args[0] ? parseInt(args[0], 10) : 1;
-
- const maxQueuePage = Math.ceil((queueSize + 1) / PAGE_SIZE);
-
- if (queuePage > maxQueuePage) {
- await msg.channel.send(errorMsg('the queue isn\'t that big'));
- return;
- }
-
- const embed = new MessageEmbed();
-
- embed.setTitle(currentlyPlaying.title);
- embed.setURL(`https://www.youtube.com/watch?v=${currentlyPlaying.url.length === 11 ? currentlyPlaying.url : getYouTubeID(currentlyPlaying.url) ?? ''}`);
-
- let description = player.status === STATUS.PLAYING ? 'âšī¸' : 'â–ļī¸';
- description += ' ';
- description += getProgressBar(20, player.getPosition() / currentlyPlaying.length);
- description += ' ';
- description += `\`[${prettyTime(player.getPosition())}/${currentlyPlaying.isLive ? 'live' : prettyTime(currentlyPlaying.length)}]\``;
- description += ' 🔉';
- description += player.isQueueEmpty() ? '' : '\n\n**Next up:**';
-
- embed.setDescription(description);
-
- let footer = `Source: ${currentlyPlaying.artist}`;
-
- if (currentlyPlaying.playlist) {
- footer += ` (${currentlyPlaying.playlist.title})`;
- }
-
- embed.setFooter({text: footer});
-
- const queuePageBegin = (queuePage - 1) * PAGE_SIZE;
- const queuePageEnd = queuePageBegin + PAGE_SIZE;
-
- player.getQueue().slice(queuePageBegin, queuePageEnd).forEach((song, i) => {
- embed.addField(`${(i + 1 + queuePageBegin).toString()}/${queueSize.toString()}`, song.title, false);
- });
-
- embed.addField('Page', `${queuePage} out of ${maxQueuePage}`, false);
+ const embed = buildQueueEmbed(player, args[0] ? parseInt(args[0], 10) : 1);
- await msg.channel.send({embeds: [embed]});
- } else {
- await msg.channel.send('queue empty');
- }
+ await msg.channel.send({embeds: [embed]});
}
}
diff --git a/src/commands/skip.ts b/src/commands/skip.ts
index 4bab307..ad9d4dc 100644
--- a/src/commands/skip.ts
+++ b/src/commands/skip.ts
@@ -5,6 +5,7 @@ import PlayerManager from '../managers/player.js';
import Command from '.';
import LoadingMessage from '../utils/loading-message.js';
import errorMsg from '../utils/error-msg.js';
+import {buildPlayingMessageEmbed} from '../utils/build-embed.js';
@injectable()
export default class implements Command {
@@ -39,10 +40,21 @@ export default class implements Command {
try {
await loader.start();
await player.forward(numToSkip);
-
- await loader.stop('keep \'er movin\'');
} catch (_: unknown) {
await loader.stop(errorMsg('no song to skip to'));
+ return;
}
+
+ const promises = [
+ loader.stop('keep \'er movin\''),
+ ];
+
+ if (player.getCurrent()) {
+ promises.push(msg.channel.send({
+ embeds: [buildPlayingMessageEmbed(player)],
+ }));
+ }
+
+ await Promise.all(promises);
}
}
diff --git a/src/commands/unskip.ts b/src/commands/unskip.ts
index a453448..adc44e7 100644
--- a/src/commands/unskip.ts
+++ b/src/commands/unskip.ts
@@ -4,6 +4,7 @@ import {inject, injectable} from 'inversify';
import PlayerManager from '../managers/player.js';
import errorMsg from '../utils/error-msg.js';
import Command from '.';
+import {buildPlayingMessageEmbed} from '../utils/build-embed.js';
@injectable()
export default class implements Command {
@@ -26,10 +27,14 @@ export default class implements Command {
try {
await player.back();
-
- await msg.channel.send('back \'er up\'');
} catch (_: unknown) {
await msg.channel.send(errorMsg('no song to go back to'));
+ return;
}
+
+ await msg.channel.send({
+ content: 'back \'er up\'',
+ embeds: [buildPlayingMessageEmbed(player)],
+ });
}
}
diff --git a/src/services/get-songs.ts b/src/services/get-songs.ts
index 780f7ec..fb1b4ea 100644
--- a/src/services/get-songs.ts
+++ b/src/services/get-songs.ts
@@ -16,7 +16,7 @@ import ThirdParty from './third-party.js';
import Config from './config.js';
import KeyValueCacheProvider from './key-value-cache.js';
-type QueuedSongWithoutChannel = Except<QueuedSong, 'addedInChannelId'>;
+type SongMetadata = Except<QueuedSong, 'addedInChannelId' | 'requestedBy'>;
const ONE_HOUR_IN_SECONDS = 60 * 60;
const ONE_MINUTE_IN_SECONDS = 1 * 60;
@@ -42,7 +42,7 @@ export default class {
this.ytsrQueue = new PQueue({concurrency: 4});
}
- async youtubeVideoSearch(query: string): Promise<QueuedSongWithoutChannel> {
+ async youtubeVideoSearch(query: string): Promise<SongMetadata> {
const {items} = await this.ytsrQueue.add(async () => this.cache.wrap(
ytsr,
query,
@@ -70,7 +70,7 @@ export default class {
return this.youtubeVideo(firstVideo.id);
}
- async youtubeVideo(url: string): Promise<QueuedSongWithoutChannel> {
+ async youtubeVideo(url: string): Promise<SongMetadata> {
const videoDetails = await this.cache.wrap(
this.youtube.videos.get,
cleanUrl(url),
@@ -86,10 +86,11 @@ export default class {
url: videoDetails.id,
playlist: null,
isLive: videoDetails.snippet.liveBroadcastContent === 'live',
+ thumbnailUrl: videoDetails.snippet.thumbnails.medium.url,
};
}
- async youtubePlaylist(listId: string): Promise<QueuedSongWithoutChannel[]> {
+ async youtubePlaylist(listId: string): Promise<SongMetadata[]> {
// YouTube playlist
const playlist = await this.cache.wrap(
this.youtube.playlists.get,
@@ -158,7 +159,7 @@ export default class {
const queuedPlaylist = {title: playlist.snippet.title, source: playlist.id};
- const songsToReturn: QueuedSongWithoutChannel[] = [];
+ const songsToReturn: SongMetadata[] = [];
for (const video of playlistVideos) {
try {
@@ -171,6 +172,7 @@ export default class {
url: video.contentDetails.videoId,
playlist: queuedPlaylist,
isLive: false,
+ thumbnailUrl: video.snippet.thumbnails.medium.url,
});
} catch (_: unknown) {
// Private and deleted videos are sometimes in playlists, duration of these is not returned and they should not be added to the queue.
@@ -180,7 +182,7 @@ export default class {
return songsToReturn;
}
- async spotifySource(url: string, playlistLimit: number): Promise<[QueuedSongWithoutChannel[], number, number]> {
+ async spotifySource(url: string, playlistLimit: number): Promise<[SongMetadata[], number, number]> {
const parsed = spotifyURI.parse(url);
let tracks: SpotifyApi.TrackObjectSimplified[] = [];
@@ -258,7 +260,7 @@ export default class {
let nSongsNotFound = 0;
// Get rid of null values
- songs = songs.reduce((accum: QueuedSongWithoutChannel[], song) => {
+ songs = songs.reduce((accum: SongMetadata[], song) => {
if (song) {
accum.push(song);
} else {
@@ -268,10 +270,10 @@ export default class {
return accum;
}, []);
- return [songs as QueuedSongWithoutChannel[], nSongsNotFound, originalNSongs];
+ return [songs as SongMetadata[], nSongsNotFound, originalNSongs];
}
- private async spotifyToYouTube(track: SpotifyApi.TrackObjectSimplified, _: QueuedPlaylist | null): Promise<QueuedSongWithoutChannel> {
+ private async spotifyToYouTube(track: SpotifyApi.TrackObjectSimplified, _: QueuedPlaylist | null): Promise<SongMetadata> {
return this.youtubeVideoSearch(`"${track.name}" "${track.artists[0].name}"`);
}
}
diff --git a/src/services/natural-language-commands.ts b/src/services/natural-language-commands.ts
index 1401eac..4ce8ea3 100644
--- a/src/services/natural-language-commands.ts
+++ b/src/services/natural-language-commands.ts
@@ -32,6 +32,8 @@ export default class {
playlist: null,
isLive: false,
addedInChannelId: msg.channel.id,
+ thumbnailUrl: null,
+ requestedBy: msg.author.id,
}, 8, 10),
]);
@@ -49,6 +51,8 @@ export default class {
playlist: null,
isLive: false,
addedInChannelId: msg.channel.id,
+ thumbnailUrl: null,
+ requestedBy: msg.author.id,
}, 358, 5.5),
]);
@@ -66,6 +70,8 @@ export default class {
playlist: null,
isLive: false,
addedInChannelId: msg.channel.id,
+ thumbnailUrl: null,
+ requestedBy: msg.author.id,
}, 50, 13),
]);
diff --git a/src/services/player.ts b/src/services/player.ts
index cee59bc..fc4b989 100644
--- a/src/services/player.ts
+++ b/src/services/player.ts
@@ -22,6 +22,8 @@ export interface QueuedSong {
playlist: QueuedPlaylist | null;
isLive: boolean;
addedInChannelId: Snowflake;
+ thumbnailUrl: string | null;
+ requestedBy: string;
}
export enum STATUS {
diff --git a/src/utils/build-embed.ts b/src/utils/build-embed.ts
new file mode 100644
index 0000000..5a2f758
--- /dev/null
+++ b/src/utils/build-embed.ts
@@ -0,0 +1,127 @@
+import getYouTubeID from 'get-youtube-id';
+import {MessageEmbed} from 'discord.js';
+import Player, {QueuedSong, STATUS} from '../services/player.js';
+import getProgressBar from './get-progress-bar.js';
+import {prettyTime} from './time.js';
+import {truncate} from './string.js';
+
+const PAGE_SIZE = 10;
+
+const getMaxSongTitleLength = (title: string) => {
+ // eslint-disable-next-line no-control-regex
+ const nonASCII = /[^\x00-\x7F]+/;
+ return nonASCII.test(title) ? 28 : 48;
+};
+
+const getSongTitle = ({title, url}: QueuedSong, shouldTruncate = false) => {
+ const cleanSongTitle = title.replace(/\[.*\]/, '').trim();
+
+ const songTitle = shouldTruncate ? truncate(cleanSongTitle, getMaxSongTitleLength(cleanSongTitle)) : cleanSongTitle;
+ const youtubeId = url.length === 11 ? url : getYouTubeID(url) ?? '';
+
+ return `[${songTitle}](https://www.youtube.com/watch?v=${youtubeId})`;
+};
+
+const getQueueInfo = (player: Player) => {
+ const queueSize = player.queueSize();
+ if (queueSize === 0) {
+ return '-';
+ }
+
+ return queueSize === 1 ? '1 song' : `${queueSize} songs`;
+};
+
+const getPlayerUI = (player: Player) => {
+ const song = player.getCurrent();
+
+ if (!song) {
+ return '';
+ }
+
+ const position = player.getPosition();
+ const button = player.status === STATUS.PLAYING ? 'âšī¸' : 'â–ļī¸';
+ const progressBar = getProgressBar(15, position / song.length);
+ const elapsedTime = `${prettyTime(position)}/${song.isLive ? 'live' : prettyTime(song.length)}`;
+
+ return `${button} ${progressBar} \`[${elapsedTime}]\` 🔉`;
+};
+
+export const buildPlayingMessageEmbed = (player: Player): MessageEmbed => {
+ const currentlyPlaying = player.getCurrent();
+
+ if (!currentlyPlaying) {
+ throw new Error('No playing song found');
+ }
+
+ const {artist, thumbnailUrl, requestedBy} = currentlyPlaying;
+ const message = new MessageEmbed();
+
+ message
+ .setColor('DARK_GREEN')
+ .setTitle('Now Playing')
+ .setDescription(`
+ **${getSongTitle(currentlyPlaying)}**
+ Requested by: <@${requestedBy}>\n
+ ${getPlayerUI(player)}
+ `)
+ .setFooter({text: `Source: ${artist}`});
+
+ if (thumbnailUrl) {
+ message.setThumbnail(thumbnailUrl);
+ }
+
+ return message;
+};
+
+export const buildQueueEmbed = (player: Player, page: number): MessageEmbed => {
+ const currentlyPlaying = player.getCurrent();
+
+ if (!currentlyPlaying) {
+ throw new Error('queue is empty');
+ }
+
+ const queueSize = player.queueSize();
+ const maxQueuePage = Math.ceil((queueSize + 1) / PAGE_SIZE);
+
+ if (page > maxQueuePage) {
+ throw new Error('the queue isn\'t that big');
+ }
+
+ const queuePageBegin = (page - 1) * PAGE_SIZE;
+ const queuePageEnd = queuePageBegin + PAGE_SIZE;
+ const queuedSongs = player
+ .getQueue()
+ .slice(queuePageBegin, queuePageEnd)
+ .map((song, index) => `\`${index + 1 + queuePageBegin}.\` ${getSongTitle(song, true)} \`[${prettyTime(song.length)}]\``)
+ .join('\n');
+
+ const {artist, thumbnailUrl, playlist, requestedBy} = currentlyPlaying;
+ const playlistTitle = playlist ? `(${playlist.title})` : '';
+ const totalLength = player.getQueue().reduce((accumulator, current) => accumulator + current.length, 0);
+
+ const message = new MessageEmbed();
+
+ let description = `**${getSongTitle(currentlyPlaying)}**\n`;
+ description += `Requested by: <@${requestedBy}>\n\n`;
+ description += `${getPlayerUI(player)}\n\n`;
+
+ if (player.getQueue().length > 0) {
+ description += '**Up next:**\n';
+ description += queuedSongs;
+ }
+
+ message
+ .setTitle(player.status === STATUS.PLAYING ? 'Now Playing' : 'Queued songs')
+ .setColor(player.status === STATUS.PLAYING ? 'DARK_GREEN' : 'NOT_QUITE_BLACK')
+ .setDescription(description)
+ .addField('In queue', getQueueInfo(player), true)
+ .addField('Total length', `${totalLength > 0 ? prettyTime(totalLength) : '-'}`, true)
+ .addField('Page', `${page} out of ${maxQueuePage}`, true)
+ .setFooter({text: `Source: ${artist} ${playlistTitle}`});
+
+ if (thumbnailUrl) {
+ message.setThumbnail(thumbnailUrl);
+ }
+
+ return message;
+};
diff --git a/src/utils/string.ts b/src/utils/string.ts
new file mode 100644
index 0000000..0b49114
--- /dev/null
+++ b/src/utils/string.ts
@@ -0,0 +1,2 @@
+export const truncate = (text: string, maxLength = 50) =>
+ text.length > maxLength ? `${text.slice(0, maxLength - 3)}...` : text;
diff --git a/yarn.lock b/yarn.lock
index 7ceb073..2a960d1 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -3740,7 +3740,16 @@ youtube.ts@^0.2.5:
axios "^0.19.0"
ytdl-core "^4.9.1"
-ytdl-core@^4.9.1, ytdl-core@^4.9.2:
+ytdl-core@^4.10.0:
+ version "4.10.0"
+ resolved "https://registry.yarnpkg.com/ytdl-core/-/ytdl-core-4.10.0.tgz#0835cb411677684539fac2bcc10553f6f58db3e1"
+ integrity sha512-RCCoSVTmMeBPH5NFR1fh3nkDU9okvWM0ZdN6plw6I5+vBBZVUEpOt8vjbSgprLRMmGUsmrQZJhvG1CHOat4mLA==
+ dependencies:
+ m3u8stream "^0.8.4"
+ miniget "^4.0.0"
+ sax "^1.1.3"
+
+ytdl-core@^4.9.1:
version "4.9.2"
resolved "https://registry.yarnpkg.com/ytdl-core/-/ytdl-core-4.9.2.tgz#c2d1ec44ee3cabff35e5843c6831755e69ffacf0"
integrity sha512-aTlsvsN++03MuOtyVD4DRF9Z/9UAeeuiNbjs+LjQBAiw4Hrdp48T3U9vAmRPyvREzupraY8pqRoBfKGqpq+eHA==
@@ -3749,10 +3758,10 @@ ytdl-core@^4.9.1, ytdl-core@^4.9.2:
miniget "^4.0.0"
sax "^1.1.3"
-ytsr@^3.5.3:
- version "3.5.3"
- resolved "https://registry.yarnpkg.com/ytsr/-/ytsr-3.5.3.tgz#88e8e2df11ce53c28b456b5510272495cb42ac3a"
- integrity sha512-BEyIKbQULmk27hiVUQ1cBszAqP8roPBOQTWPZpBioKxjSZBeicfgF2qPIQoY7koodQwRuo1DmCFz3DyrXjADxg==
+ytsr@^3.6.0:
+ version "3.6.0"
+ resolved "https://registry.yarnpkg.com/ytsr/-/ytsr-3.6.0.tgz#bc55e8957dcc293e49e18cc3b3e6d2890d15a15e"
+ integrity sha512-3fN8lxL+JHtp2xEZoAK3AeTjNm5WB4MH6n2OxHNxP06xQtuO5khbLwh6IJGiZRNi/v3de+jYYbctp2pUqNT/Qw==
dependencies:
miniget "^4.2.1"