From 59cbc8b474425225f7f188ce54bc19d5f1193ecb Mon Sep 17 00:00:00 2001 From: Thongrapee Panyapatiphan Date: Sat, 22 Jan 2022 01:50:57 +0700 Subject: [PATCH 1/8] Announce current song (#470) Co-authored-by: Max Isom --- CHANGELOG.md | 2 + package.json | 4 +- src/commands/play.ts | 240 ++++++++++++---------- src/commands/queue.ts | 60 +----- src/commands/skip.ts | 16 +- src/commands/unskip.ts | 9 +- src/services/get-songs.ts | 20 +- src/services/natural-language-commands.ts | 6 + src/services/player.ts | 2 + src/utils/build-embed.ts | 127 ++++++++++++ src/utils/string.ts | 2 + yarn.lock | 19 +- 12 files changed, 317 insertions(+), 190 deletions(-) create mode 100644 src/utils/build-embed.ts create mode 100644 src/utils/string.ts 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); - - 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; - } - - // Must be resuming play - if (!wasPlayingSong) { - await res.stop(errorMsg('nothing to play')); - return; - } - - await player.connect(targetVoiceChannel); - await player.play(); - - await res.stop('the stop-and-go light is now green'); - return; - } - - const addToFrontOfQueue = args[args.length - 1] === 'i' || args[args.length - 1] === 'immediate'; - const shuffleAdditions = args[args.length - 1] === 's' || args[args.length - 1] === 'shuffle'; - - let newSongs: Array> = []; - let extraMsg = ''; - - // Test if it's a complete URL try { - const url = new URL(args[0]); + const player = this.playerManager.get(msg.guild!.id); - const YOUTUBE_HOSTS = [ - 'www.youtube.com', - 'youtu.be', - 'youtube.com', - 'music.youtube.com', - 'www.music.youtube.com', - ]; + const wasPlayingSong = player.getCurrent() !== null; - if (YOUTUBE_HOSTS.includes(url.host)) { - // YouTube source - 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); - } 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); - - if (totalSongs > playlistLimit) { - extraMsg = `a random sample of ${playlistLimit} songs was taken`; + if (args.length === 0) { + if (player.status === STATUS.PLAYING) { + await res.stop(errorMsg('already playing, give me a song name')); + return; } - if (totalSongs > playlistLimit && nSongsNotFound !== 0) { - extraMsg += ' and '; + // Must be resuming play + if (!wasPlayingSong) { + await res.stop(errorMsg('nothing to play')); + return; } - if (nSongsNotFound !== 0) { - if (nSongsNotFound === 1) { - extraMsg += '1 song was not found'; - } else { - extraMsg += `${nSongsNotFound.toString()} songs were not found`; - } - } + await player.connect(targetVoiceChannel); + await player.play(); - newSongs.push(...convertedSongs); - } - } catch (_: unknown) { - // Not a URL, must search YouTube - const query = addToFrontOfQueue ? args.slice(0, args.length - 1).join(' ') : args.join(' '); + await Promise.all([ + res.stop('the stop-and-go light is now green'), + msg.channel.send({embeds: [buildPlayingMessageEmbed(player)]}), + ]); - const song = await this.getSongs.youtubeVideoSearch(query); - - 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; - } + const addToFrontOfQueue = args[args.length - 1] === 'i' || args[args.length - 1] === 'immediate'; + const shuffleAdditions = args[args.length - 1] === 's' || args[args.length - 1] === 'shuffle'; - if (shuffleAdditions) { - newSongs = shuffle(newSongs); - } + let newSongs: Array> = []; + let extraMsg = ''; - newSongs.forEach(song => { - player.add({...song, addedInChannelId: msg.channel.id}, {immediate: addToFrontOfQueue}); - }); + // Test if it's a complete URL + try { + const url = new URL(args[0]); - const firstSong = newSongs[0]; + const YOUTUBE_HOSTS = [ + 'www.youtube.com', + 'youtu.be', + 'youtube.com', + 'music.youtube.com', + 'www.music.youtube.com', + ]; - let statusMsg = ''; + if (YOUTUBE_HOSTS.includes(url.host)) { + // YouTube source + 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 (player.voiceConnection === null) { - await player.connect(targetVoiceChannel); + 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); - // Resume / start playback - await player.play(); + if (totalSongs > playlistLimit) { + extraMsg = `a random sample of ${playlistLimit} songs was taken`; + } - if (wasPlayingSong) { - statusMsg = 'resuming playback'; + 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`; + } + } + + newSongs.push(...convertedSongs); + } + } catch (_: unknown) { + // Not a URL, must search YouTube + const query = addToFrontOfQueue ? args.slice(0, args.length - 1).join(' ') : args.join(' '); + + const song = await this.getSongs.youtubeVideoSearch(query); + + if (song) { + newSongs.push(song); + } else { + await res.stop(errorMsg('that doesn\'t exist')); + return; + } } - } - // Build response message - if (statusMsg !== '') { - if (extraMsg === '') { - extraMsg = statusMsg; + if (newSongs.length === 0) { + await res.stop(errorMsg('no songs found')); + return; + } + + if (shuffleAdditions) { + newSongs = shuffle(newSongs); + } + + newSongs.forEach(song => { + player.add({...song, addedInChannelId: msg.channel.id, requestedBy: msg.author.id}, {immediate: addToFrontOfQueue}); + }); + + const firstSong = newSongs[0]; + + let statusMsg = ''; + + if (player.voiceConnection === null) { + await player.connect(targetVoiceChannel); + + // Resume / start playback + await player.play(); + + 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}`; + } + } + + 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 { - extraMsg = `${statusMsg}, ${extraMsg}`; + await res.stop(`u betcha, **${firstSong.title}** and ${newSongs.length - 1} other songs were added to the queue${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}`); + } 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 { const player = this.playerManager.get(msg.guild!.id); - const currentlyPlaying = player.getCurrent(); + const embed = buildQueueEmbed(player, args[0] ? parseInt(args[0], 10) : 1); - 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); - - 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; +type SongMetadata = Except; 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 { + async youtubeVideoSearch(query: string): Promise { 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 { + async youtubeVideo(url: string): Promise { 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 { + async youtubePlaylist(listId: string): Promise { // 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 { + private async spotifyToYouTube(track: SpotifyApi.TrackObjectSimplified, _: QueuedPlaylist | null): Promise { 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" From 8041586c31dbfcaf4f2b12109e5dcfac27f61892 Mon Sep 17 00:00:00 2001 From: Max Isom Date: Fri, 21 Jan 2022 12:51:21 -0600 Subject: [PATCH 2/8] Release 0.5.0 --- CHANGELOG.md | 5 ++++- package.json | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d868790..a1e9c2c 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] + +## [0.5.0] - 2022-01-21 ### Changed - Queue embeds are now more detailed and appear when resuming playback. Thanks @bokherus! @@ -44,7 +46,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Initial release -[Unreleased]: https://github.com/codetheweb/muse/compare/v0.4.0...HEAD +[Unreleased]: https://github.com/codetheweb/muse/compare/v0.5.0...HEAD +[0.5.0]: https://github.com/codetheweb/muse/compare/v0.4.0...v0.5.0 [0.4.0]: https://github.com/codetheweb/muse/compare/v0.3.2...v0.4.0 [0.3.2]: https://github.com/codetheweb/muse/compare/v0.3.1...v0.3.2 [0.3.1]: https://github.com/codetheweb/muse/compare/v0.3.0...v0.3.1 diff --git a/package.json b/package.json index 1a36193..2e352c3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "muse", - "version": "0.4.0", + "version": "0.5.0", "description": "🎧 a self-hosted Discord music bot that doesn't suck ", "exports": "./dist/src/index.js", "repository": "git@github.com:codetheweb/muse.git", From 1081b1bce718dfe13b389a5de509214cd21e12b4 Mon Sep 17 00:00:00 2001 From: Thongrapee Panyapatiphan Date: Sat, 22 Jan 2022 08:20:23 +0700 Subject: [PATCH 3/8] Reduce container image size (#485) Co-authored-by: Max Isom --- CHANGELOG.md | 2 ++ Dockerfile | 8 ++++---- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a1e9c2c..9fbfeba 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] +### Fixed +- Switched back to an Alpine container base image to reduce size ## [0.5.0] - 2022-01-21 ### Changed diff --git a/Dockerfile b/Dockerfile index d1310ca..63f0f62 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,9 +1,9 @@ -FROM node:16.13.0 AS base +FROM node:16.13.2-alpine AS base # Install ffmpeg -RUN apt-get update && \ - apt-get install -y ffmpeg && \ - rm -rf /var/lib/apt/lists/* +RUN apk update && \ + apk add ffmpeg && \ + rm -rf /var/cache/apk/* WORKDIR /usr/app From 9c1ca244b24017fa89168a40585b10d07f9cd308 Mon Sep 17 00:00:00 2001 From: Max Isom Date: Tue, 25 Jan 2022 20:15:14 -0500 Subject: [PATCH 4/8] Check out correct branch --- .github/workflows/pr.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 02eb197..ab5db0a 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -26,7 +26,7 @@ jobs: uses: actions/cache@v2 with: path: /tmp/.buildx-cache - key: ${{ runner.os }}-buildx-prs-${{ matrix.build-arch }}-${{ github.sha }} + key: ${{ runner.os }}-buildx-prs-${{ matrix.build-arch }}-${{ github.event.pull_request.head.sha }} restore-keys: | ${{ runner.os }}-buildx-prs-${{ matrix.build-arch }} @@ -36,6 +36,10 @@ jobs: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} + - uses: actions/checkout@v2 + with: + ref: ${{ github.event.pull_request.head.sha }} + - name: Build and push id: docker_build uses: docker/build-push-action@v2 From c89bd278d3fe473dce1ba5700e521750722aefab Mon Sep 17 00:00:00 2001 From: Max Isom Date: Tue, 25 Jan 2022 20:16:29 -0500 Subject: [PATCH 5/8] Revert "Reduce container image size (#485)" This reverts commit 1081b1bce718dfe13b389a5de509214cd21e12b4. --- CHANGELOG.md | 2 -- Dockerfile | 8 ++++---- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9fbfeba..a1e9c2c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,8 +5,6 @@ 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] -### Fixed -- Switched back to an Alpine container base image to reduce size ## [0.5.0] - 2022-01-21 ### Changed diff --git a/Dockerfile b/Dockerfile index 63f0f62..d1310ca 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,9 +1,9 @@ -FROM node:16.13.2-alpine AS base +FROM node:16.13.0 AS base # Install ffmpeg -RUN apk update && \ - apk add ffmpeg && \ - rm -rf /var/cache/apk/* +RUN apt-get update && \ + apt-get install -y ffmpeg && \ + rm -rf /var/lib/apt/lists/* WORKDIR /usr/app From 9daf12668023c8902fe7aea4f610c3d51fff4880 Mon Sep 17 00:00:00 2001 From: Thongrapee Panyapatiphan Date: Wed, 26 Jan 2022 08:18:01 +0700 Subject: [PATCH 6/8] Fix whole playlist fails when one song is unavailable (#489) Co-authored-by: Max Isom --- CHANGELOG.md | 2 ++ src/services/get-songs.ts | 17 ++++++++++------- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a1e9c2c..0282b6d 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] +### Fixed +- Queueing Spotify playlists could sometimes fail when a song wasn't found on YouTube ## [0.5.0] - 2022-01-21 ### Changed diff --git a/src/services/get-songs.ts b/src/services/get-songs.ts index fb1b4ea..e132d7b 100644 --- a/src/services/get-songs.ts +++ b/src/services/get-songs.ts @@ -255,14 +255,17 @@ export default class { tracks = shuffled.slice(0, playlistLimit); } - let songs = await Promise.all(tracks.map(async track => this.spotifyToYouTube(track, playlist))); + const searchResults = await Promise.allSettled(tracks.map(async track => this.spotifyToYouTube(track))); let nSongsNotFound = 0; - // Get rid of null values - songs = songs.reduce((accum: SongMetadata[], song) => { - if (song) { - accum.push(song); + // Count songs that couldn't be found + const songs: SongMetadata[] = searchResults.reduce((accum: SongMetadata[], result) => { + if (result.status === 'fulfilled') { + accum.push({ + ...result.value, + ...(playlist ? {playlist} : {}), + }); } else { nSongsNotFound++; } @@ -270,10 +273,10 @@ export default class { return accum; }, []); - return [songs as SongMetadata[], nSongsNotFound, originalNSongs]; + return [songs, nSongsNotFound, originalNSongs]; } - private async spotifyToYouTube(track: SpotifyApi.TrackObjectSimplified, _: QueuedPlaylist | null): Promise { + private async spotifyToYouTube(track: SpotifyApi.TrackObjectSimplified): Promise { return this.youtubeVideoSearch(`"${track.name}" "${track.artists[0].name}"`); } } From 15038c447789e3c24db0b43c3be7075cda0cc002 Mon Sep 17 00:00:00 2001 From: Max Isom Date: Tue, 25 Jan 2022 20:18:27 -0500 Subject: [PATCH 7/8] Release 0.5.1 --- CHANGELOG.md | 5 ++++- package.json | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0282b6d..7330957 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] + +## [0.5.1] - 2022-01-25 ### Fixed - Queueing Spotify playlists could sometimes fail when a song wasn't found on YouTube @@ -48,7 +50,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Initial release -[Unreleased]: https://github.com/codetheweb/muse/compare/v0.5.0...HEAD +[Unreleased]: https://github.com/codetheweb/muse/compare/v0.5.1...HEAD +[0.5.1]: https://github.com/codetheweb/muse/compare/v0.5.0...v0.5.1 [0.5.0]: https://github.com/codetheweb/muse/compare/v0.4.0...v0.5.0 [0.4.0]: https://github.com/codetheweb/muse/compare/v0.3.2...v0.4.0 [0.3.2]: https://github.com/codetheweb/muse/compare/v0.3.1...v0.3.2 diff --git a/package.json b/package.json index 2e352c3..f98475b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "muse", - "version": "0.5.0", + "version": "0.5.1", "description": "🎧 a self-hosted Discord music bot that doesn't suck ", "exports": "./dist/src/index.js", "repository": "git@github.com:codetheweb/muse.git", From af05210be4a8857ea707866192efa79b3945b314 Mon Sep 17 00:00:00 2001 From: Max Isom Date: Tue, 25 Jan 2022 20:31:56 -0500 Subject: [PATCH 8/8] Use correct context --- .github/workflows/pr.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index ab5db0a..a332a28 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -44,6 +44,7 @@ jobs: id: docker_build uses: docker/build-push-action@v2 with: + context: . push: true tags: codetheweb/muse:${{ github.event.pull_request.head.sha }}-${{ matrix.tagged-platform }} cache-from: type=local,src=/tmp/.buildx-cache