From 59cbc8b474425225f7f188ce54bc19d5f1193ecb Mon Sep 17 00:00:00 2001 From: Thongrapee Panyapatiphan Date: Sat, 22 Jan 2022 01:50:57 +0700 Subject: [PATCH] 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"