diff --git a/.gitignore b/.gitignore index c4e1f82..d02ad80 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,7 @@ node_modules/ test/ # Compiled -lib/ \ No newline at end of file +lib/ + +# error logs +yarn-error.log \ No newline at end of file diff --git a/package.json b/package.json index 5023efc..d8ccef4 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,8 @@ "homepage": "https://github.com/Androz2091/discord-player#readme", "dependencies": { "discord-ytdl-core": "^5.0.2", + "soundcloud-scraper": "^4.0.3", + "spotify-url-info": "^2.2.0", "youtube-sr": "^4.0.3", "ytdl-core": "^4.5.0" }, diff --git a/src/Player.ts b/src/Player.ts index 9abfd97..414d691 100644 --- a/src/Player.ts +++ b/src/Player.ts @@ -1,11 +1,27 @@ -import { EventEmitter } from 'events'; -import { Client } from 'discord.js'; -import { PlayerOptions } from './types/Player'; -import Util from './Util'; +import YouTube from "youtube-sr"; +import { EventEmitter } from "events"; +import { Client, Collection, Snowflake, Collector, Message } from "discord.js"; +import { PlayerOptions } from "./types/types"; +import Util from "./utils/Util"; +import AudioFilters from "./utils/AudioFilters"; +import Queue from "./Structures/Queue"; +import Track from "./Structures/Track"; +import { PlayerEvents } from "./utils/Constants"; + +// @ts-ignore +import spotify from "spotify-url-info"; +// @ts-ignore +import { Client as SoundCloudClient } from "soundcloud-scraper"; + +const SoundCloud = new SoundCloudClient; export default class Player extends EventEmitter { public client!: Client; public options: PlayerOptions; + public filters: typeof AudioFilters; + public queues: Collection; + private _resultsCollectors: Collection>; + private _cooldownsTimeout: Collection; constructor(client: Client, options?: PlayerOptions) { super(); @@ -13,7 +29,7 @@ export default class Player extends EventEmitter { /** * The discord client that instantiated this player */ - Object.defineProperty(this, 'client', { + Object.defineProperty(this, "client", { value: client, enumerable: false }); @@ -25,5 +41,99 @@ export default class Player extends EventEmitter { // check FFmpeg void Util.alertFFmpeg(); + + /** + * The audio filters + */ + this.filters = AudioFilters; + + /** + * Player queues + */ + this.queues = new Collection(); + } + + static get AudioFilters() { + return AudioFilters; + } + + private _searchTracks(message: Message, query: string, firstResult?: boolean, isAttachment?: boolean): Promise { + return new Promise(async (resolve) => { + let tracks: Track[] = []; + let queryType = Util.getQueryType(query); + + switch(queryType) { + case "soundcloud_track": { + const data = await SoundCloud.getSongInfo(query).catch(() => { }) + if (data) { + const track = new Track(this, { + title: data.title, + url: data.url, + duration: Util.durationString(Util.parseMS(data.duration / 1000)), + description: data.description, + thumbnail: data.thumbnail, + views: data.playCount, + author: data.author, + requestedBy: message.author, + fromPlaylist: false, + source: "soundcloud", + engine: data + }); + + tracks.push(track) + } + } + break; + case "spotify_song": { + const matchSpotifyURL = query.match(/https?:\/\/(?:embed\.|open\.)(?:spotify\.com\/)(?:track\/|\?uri=spotify:track:)((\w|-){22})/) + if (matchSpotifyURL) { + const spotifyData = await spotify.getPreview(query).catch(() => { }) + if (spotifyData) { + tracks = await Util.ytSearch(`${spotifyData.artist} - ${spotifyData.title}`, { user: message.author, player: this }); + } + } + } + break; + default: + tracks = await Util.ytSearch(query, { user: message.author, player: this }); + } + + if (tracks.length < 1) return this.emit(PlayerEvents.NO_RESULTS, message, query); + if (firstResult) return resolve(tracks[0]); + + const collectorString = `${message.author.id}-${message.channel.id}`; + const currentCollector = this._resultsCollectors.get(collectorString); + if (currentCollector) currentCollector.stop() + + const collector = message.channel.createMessageCollector((m) => m.author.id === message.author.id, { + time: 60000 + }); + + this._resultsCollectors.set(collectorString, collector); + + this.emit(PlayerEvents.SEARCH_RESULTS, message, query, tracks, collector); + + collector.on("collect", ({ content }) => { + if (content === "cancel") { + collector.stop(); + return this.emit(PlayerEvents.SEARCH_CANCEL, message, query, tracks); + } + + if (!isNaN(content) && parseInt(content) >= 1 && parseInt(content) <= tracks.length) { + const index = parseInt(content, 10); + const track = tracks[index - 1]; + collector.stop(); + resolve(track); + } else { + this.emit(PlayerEvents.SEARCH_INVALID_RESPONSE, message, query, tracks, content, collector); + } + }) + + collector.on("end", (collected, reason) => { + if (reason === "time") { + this.emit(PlayerEvents.SEARCH_CANCEL, message, query, tracks); + } + }); + }); } } diff --git a/src/Structures/Queue.ts b/src/Structures/Queue.ts index c2880ce..c853b5c 100644 --- a/src/Structures/Queue.ts +++ b/src/Structures/Queue.ts @@ -1,12 +1,129 @@ -import Player from '../Player'; +import { Message, Snowflake, VoiceConnection } from "discord.js"; +import AudioFilters from "../utils/AudioFilters"; +import Player from "../Player"; +import { EventEmitter } from "events"; +import Track from "./Track"; +import { QueueFilters } from "../types/types"; -export default class Queue { +export default class Queue extends EventEmitter { public player!: Player; + public guildID: Snowflake; + public voiceConnection?: VoiceConnection; + public stream?: any; + public tracks: Track[]; + public previousTracks: Track[]; + public stopped: boolean; + public lastSkipped: boolean; + public volume: number; + public paused: boolean; + public repeatMode: boolean; + public loopMode: boolean; + public filters: QueueFilters; + public additionalStreamTime: number; + public firstMessage: Message; + + constructor(player: Player, message: Message, filters: typeof AudioFilters) { + super(); - constructor(player: Player) { /** * The player that instantiated this Queue */ - Object.defineProperty(this, 'player', { value: player, enumerable: false }); + Object.defineProperty(this, "player", { value: player, enumerable: false }); + + /** + * ID of the guild assigned to this queue + */ + this.guildID = message.guild.id; + + /** + * The voice connection of this queue + */ + this.voiceConnection = null; + + /** + * Tracks of this queue + */ + this.tracks = []; + + /** + * Previous tracks of this queue + */ + this.previousTracks = []; + + /** + * If the player of this queue is stopped + */ + this.stopped = false; + + /** + * If last track was skipped + */ + this.lastSkipped = false; + + /** + * Queue volume + */ + this.volume = 100; + + /** + * If the player of this queue is paused + */ + this.paused = Boolean(this.voiceConnection?.dispatcher?.paused); + + /** + * If repeat mode is enabled in this queue + */ + this.repeatMode = false; + + /** + * If loop mode is enabled in this queue + */ + this.loopMode = false; + + /** + * The additional calculated stream time + */ + this.additionalStreamTime = 0; + + /** + * The initial message object + */ + this.firstMessage = message; + + // @ts-ignore + this.filters = {}; + + Object.keys(AudioFilters).forEach(fn => { + // @ts-ignore + this.filters[fn] = false; + }); + } + + /** + * Currently playing track + */ + get playing() { + return this.tracks[0]; + } + + /** + * Calculated volume of this queue + */ + get calculatedVolume() { + return this.filters.bassboost ? this.volume + 50 : this.volume; + } + + /** + * Total duration + */ + get totalTime() { + return this.tracks.length > 0 ? this.tracks.map((t) => t.durationMS).reduce((p, c) => p + c) : 0; + } + + /** + * Current stream time + */ + get currentStreamTime() { + return this.voiceConnection?.dispatcher?.streamTime + this.additionalStreamTime || 0; } } diff --git a/src/Structures/Track.ts b/src/Structures/Track.ts index 1df7e2d..970aeb4 100644 --- a/src/Structures/Track.ts +++ b/src/Structures/Track.ts @@ -1,6 +1,6 @@ -import Player from '../Player'; -import { User } from 'discord.js'; -import { TrackData } from "../types/Track"; +import Player from "../Player"; +import { User } from "discord.js"; +import { TrackData } from "../types/types"; export default class Track { public player!: Player; @@ -19,18 +19,18 @@ export default class Track { /** * The player that instantiated this Track */ - Object.defineProperty(this, 'player', { value: player, enumerable: false }); + Object.defineProperty(this, "player", { value: player, enumerable: false }); void this._patch(data); } private _patch(data: TrackData) { - this.title = data.title ?? ''; - this.description = data.description ?? ''; - this.author = data.author ?? ''; - this.url = data.url ?? ''; - this.thumbnail = data.thumbnail ?? ''; - this.duration = data.duration ?? ''; + this.title = data.title ?? ""; + this.description = data.description ?? ""; + this.author = data.author ?? ""; + this.url = data.url ?? ""; + this.thumbnail = data.thumbnail ?? ""; + this.duration = data.duration ?? ""; this.views = data.views ?? 0; this.requestedBy = data.requestedBy; this.fromPlaylist = Boolean(data.fromPlaylist); @@ -38,4 +38,28 @@ export default class Track { // raw Object.defineProperty(this, "raw", { get: () => data, enumerable: false }); } + + /** + * The queue in which this track is located + */ + get queue() { + return this.player.queues.find(q => q.tracks.includes(this)); + } + + /** + * The track duration in millisecond + */ + get durationMS() { + const times = (n: number, t: number) => { + let tn = 1; + for (let i = 0; i < t; i++) tn *= n; + return t <= 0 ? 1000 : tn * 1000; + }; + + return this.duration.split(":").reverse().map((m, i) => parseInt(m) * times(60, i)).reduce((a, c) => a + c, 0); + } + + toString() { + return `${this.title} by ${this.author}`; + } } diff --git a/src/Util.ts b/src/Util.ts deleted file mode 100644 index 8890517..0000000 --- a/src/Util.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { PlayerOptions } from './types/Player'; -import { FFmpeg } from 'prism-media'; - -export default class Util { - constructor() { - throw new Error(`The ${this.constructor.name} class is static and cannot be instantiated!`); - } - - static get DefaultPlayerOptions() { - return { - leaveOnEnd: true, - leaveOnStop: true, - leaveOnEmpty: true, - leaveOnEmptyCooldown: 0, - autoSelfDeaf: true, - enableLive: false, - ytdlDownloadOptions: {} - } as PlayerOptions; - } - - static checkFFmpeg(force?: boolean) { - try { - FFmpeg.getInfo(Boolean(force)); - - return true; - } catch { - return false; - } - } - - static alertFFmpeg() { - const hasFFmpeg = Util.checkFFmpeg(); - - if (!hasFFmpeg) - console.warn( - '[Discord Player] FFmpeg/Avconv not found! Install via "npm install ffmpeg-static" or download from https://ffmpeg.org/download.html' - ); - } -} diff --git a/src/index.ts b/src/index.ts index d1fc979..f54dbd7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,7 +1,7 @@ -export * as AudioFilters from './AudioFilters'; -export * as Player from './Player'; -export * as Util from './Util'; -export * as Track from './Structures/Track'; -export * as Queue from './Structures/Queue'; -export * from './types/Player'; -export { version } from '../package.json'; +export * as AudioFilters from "./utils/AudioFilters"; +export * as Player from "./Player"; +export * as Util from "./utils/Util"; +export * as Track from "./Structures/Track"; +export * as Queue from "./Structures/Queue"; +export * from "./types/types"; +export { version } from "../package.json"; diff --git a/src/types/Player.ts b/src/types/Player.ts deleted file mode 100644 index 427f552..0000000 --- a/src/types/Player.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { downloadOptions } from 'ytdl-core'; - -export interface PlayerOptions { - leaveOnEnd?: boolean; - leaveOnEndCooldown?: number; - leaveOnStop?: boolean; - leaveOnEmpty?: boolean; - leaveOnEmptyCooldown?: number; - autoSelfDeaf?: boolean; - enableLive?: boolean; - ytdlDownloadOptions?: downloadOptions; -} diff --git a/src/types/Track.ts b/src/types/Track.ts deleted file mode 100644 index e574813..0000000 --- a/src/types/Track.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { User } from "discord.js"; - -export interface TrackData { - title: string; - description: string; - author: string; - url: string; - thumbnail: string; - duration: string; - views: number; - requestedBy: User; - fromPlaylist: boolean; -} \ No newline at end of file diff --git a/src/types/types.ts b/src/types/types.ts new file mode 100644 index 0000000..56762fe --- /dev/null +++ b/src/types/types.ts @@ -0,0 +1,105 @@ +import { downloadOptions } from "ytdl-core"; +import { User } from "discord.js"; + +export interface PlayerOptions { + leaveOnEnd?: boolean; + leaveOnEndCooldown?: number; + leaveOnStop?: boolean; + leaveOnEmpty?: boolean; + leaveOnEmptyCooldown?: number; + autoSelfDeaf?: boolean; + enableLive?: boolean; + ytdlDownloadOptions?: downloadOptions; + useSafeSearch?: boolean; +} + +export type FiltersName = + | "bassboost" + | "8D" + | "vaporwave" + | "nightcore" + | "phaser" + | "tremolo" + | "vibrato" + | "reverse" + | "treble" + | "normalizer" + | "surrounding" + | "pulsator" + | "subboost" + | "karaoke" + | "flanger" + | "gate" + | "haas" + | "mcompand" + | "mono" + | "mstlr" + | "mstrr" + | "compressor" + | "expander" + | "softlimiter" + | "chorus" + | "chorus2d" + | "chorus3d" + | "fadein"; + +export type TrackSource = "soundcloud" | "youtube" | "arbitrary"; + +export interface TrackData { + title: string; + description: string; + author: string; + url: string; + thumbnail: string; + duration: string; + views: number; + requestedBy: User; + fromPlaylist: boolean; + source?: TrackSource; + engine?: any; +} + +export type QueueFilters = { + "bassboost": boolean; + "8D": boolean; + "vaporwave": boolean; + "nightcore": boolean; + "phaser": boolean; + "tremolo": boolean; + "vibrato": boolean; + "reverse": boolean; + "treble": boolean; + "normalizer": boolean; + "surrounding": boolean; + "pulsator": boolean; + "subboost": boolean; + "karaoke": boolean; + "flanger": boolean; + "gate": boolean; + "haas": boolean; + "mcompand": boolean; + "mono": boolean; + 'mstlr': boolean; + "mstrr": boolean; + "compressor": boolean; + "expander": boolean; + "softlimiter": boolean; + "chorus": boolean; + "chorus2d": boolean; + "chorus3d": boolean; + "fadein": boolean; +} + +export type QueryType = + | "soundcloud_track" + | "soundcloud_playlist" + | "spotify_song" + | "spotify_album" + | "spotify_playlist" + | "youtube_video" + | "youtube_playlist" + | "vimeo" + | "facebook" + | "reverbnation" + | "attachment" + | "youtube_search"; \ No newline at end of file diff --git a/src/AudioFilters.ts b/src/utils/AudioFilters.ts similarity index 80% rename from src/AudioFilters.ts rename to src/utils/AudioFilters.ts index 3274d5b..e8de4f8 100644 --- a/src/AudioFilters.ts +++ b/src/utils/AudioFilters.ts @@ -1,4 +1,6 @@ -export default { +import { FiltersName } from "../types/types"; + +const FilterList = { bassboost: 'bass=g=20', '8D': 'apulsator=hz=0.09', vaporwave: 'aresample=48000,asetrate=48000*0.8', @@ -41,6 +43,12 @@ export default { return Object.keys(this).length; }, toString() { - return `"${Object.values(this).join(',')}"`; + return `${Object.values(this).join(',')}`; + }, + create(filter?: FiltersName[]) { + if (!filter || !Array.isArray(filter)) return this.toString(); + return filter.filter(predicate => typeof predicate === "string").map(m => this[m]).join(","); } }; + +export default FilterList; \ No newline at end of file diff --git a/src/utils/Constants.ts b/src/utils/Constants.ts new file mode 100644 index 0000000..e4f21a7 --- /dev/null +++ b/src/utils/Constants.ts @@ -0,0 +1,16 @@ +export const PlayerEvents = { + BOT_DISCONNECT: "botDisconnect", + CHANNEL_EMPTY: "channelEmpty", + ERROR: "error", + NO_RESULTS: "noResults", + PLAYLIST_ADD: "playlistAdd", + PLAYLIST_PARSE_END: "playlistParseEnd", + PLAYLIST_PARSE_START: "playlistParseStart", + QUEUE_CREATE: "queueCreate", + QUEUE_END: "queueEnd", + SEARCH_CANCEL: "searchCancel", + SEARCH_INVALID_RESPONSE: "searchInvalidResponse", + SEARCH_RESULTS: "searchResults", + TRACK_ADD: "trackAdd", + TRACK_START: "trackStart", +}; \ No newline at end of file diff --git a/src/utils/Util.ts b/src/utils/Util.ts new file mode 100644 index 0000000..fa4aeb4 --- /dev/null +++ b/src/utils/Util.ts @@ -0,0 +1,116 @@ +import { PlayerOptions, QueryType } from "../types/types"; +import { FFmpeg } from "prism-media"; +import YouTube from "youtube-sr"; +import Track from "../Structures/Track"; +// @ts-ignore +import { validateURL as SoundcloudValidateURL } from "soundcloud-scraper"; + +const spotifySongRegex = (/https?:\/\/(?:embed\.|open\.)(?:spotify\.com\/)(?:track\/|\?uri=spotify:track:)((\w|-){22})/); +const spotifyPlaylistRegex = (/https?:\/\/(?:embed\.|open\.)(?:spotify\.com\/)(?:playlist\/|\?uri=spotify:playlist:)((\w|-){22})/); +const spotifyAlbumRegex = (/https?:\/\/(?:embed\.|open\.)(?:spotify\.com\/)(?:album\/|\?uri=spotify:album:)((\w|-){22})/); +const vimeoRegex = (/(http|https)?:\/\/(www\.|player\.)?vimeo\.com\/(?:channels\/(?:\w+\/)?|groups\/([^/]*)\/videos\/|video\/|)(\d+)(?:|\/\?)/); +const facebookRegex = (/(https?:\/\/)(www\.|m\.)?(facebook|fb).com\/.*\/videos\/.*/); +const reverbnationRegex = (/https:\/\/(www.)?reverbnation.com\/(.+)\/song\/(.+)/); + +export default class Util { + constructor() { + throw new Error(`The ${this.constructor.name} class is static and cannot be instantiated!`); + } + + static get DefaultPlayerOptions() { + return { + leaveOnEnd: true, + leaveOnStop: true, + leaveOnEmpty: true, + leaveOnEmptyCooldown: 0, + autoSelfDeaf: true, + enableLive: false, + ytdlDownloadOptions: {} + } as PlayerOptions; + } + + static checkFFmpeg(force?: boolean) { + try { + FFmpeg.getInfo(Boolean(force)); + + return true; + } catch { + return false; + } + } + + static alertFFmpeg() { + const hasFFmpeg = Util.checkFFmpeg(); + + if (!hasFFmpeg) + console.warn( + "[Discord Player] FFmpeg/Avconv not found! Install via \"npm install ffmpeg-static\" or download from https://ffmpeg.org/download.html" + ); + } + + static getQueryType(query: string): QueryType { + if (SoundcloudValidateURL(query) && !query.includes("/sets/")) return "soundcloud_track"; + if (SoundcloudValidateURL(query) && query.includes("/sets/")) return "soundcloud_playlist"; + if (spotifySongRegex.test(query)) return "spotify_song"; + if (spotifyAlbumRegex.test(query)) return "spotify_album"; + if (spotifyPlaylistRegex.test(query)) return "spotify_playlist"; + if (YouTube.validate(query, "VIDEO")) return "youtube_video"; + if (YouTube.validate(query, "PLAYLIST")) return "youtube_playlist"; + if (vimeoRegex.test(query)) return "vimeo"; + if (facebookRegex.test(query)) return "facebook"; + if (reverbnationRegex.test(query)) return "reverbnation"; + if (Util.isURL(query)) return "attachment"; + + return "youtube_search"; + } + + static isURL(str: string) { + const urlRegex = '^(?!mailto:)(?:(?:http|https|ftp)://)(?:\\S+(?::\\S*)?@)?(?:(?:(?:[1-9]\\d?|1\\d\\d|2[01]\\d|22[0-3])(?:\\.(?:1?\\d{1,2}|2[0-4]\\d|25[0-5])){2}(?:\\.(?:[0-9]\\d?|1\\d\\d|2[0-4]\\d|25[0-4]))|(?:(?:[a-z\\u00a1-\\uffff0-9]+-?)*[a-z\\u00a1-\\uffff0-9]+)(?:\\.(?:[a-z\\u00a1-\\uffff0-9]+-?)*[a-z\\u00a1-\\uffff0-9]+)*(?:\\.(?:[a-z\\u00a1-\\uffff]{2,})))|localhost)(?::\\d{2,5})?(?:(/|\\?|#)[^\\s]*)?$'; + const url = new RegExp(urlRegex, 'i'); + return str.length < 2083 && url.test(str); + } + + static getVimeoID(query: string) { + return Util.getQueryType(query) === "vimeo" ? query.split("/").filter(x => !!x).pop() : null + } + + static parseMS(milliseconds: number) { + // taken from ms package :: https://github.com/sindresorhus/parse-ms/blob/main/index.js + const roundTowardsZero = milliseconds > 0 ? Math.floor : Math.ceil; + + return { + days: roundTowardsZero(milliseconds / 86400000), + hours: roundTowardsZero(milliseconds / 3600000) % 24, + minutes: roundTowardsZero(milliseconds / 60000) % 60, + seconds: roundTowardsZero(milliseconds / 1000) % 60 + }; + } + + static durationString(durObj: object) { + return Object.values(durObj).map(m => isNaN(m) ? 0 : m ).join(":"); + } + + static ytSearch(query: string, options?: any): Promise { + return new Promise(async (resolve) => { + await YouTube.search(query, { + type: "video", + safeSearch: Boolean(options?.player.options.useSafeSearch) + }) + .then(results => { + resolve(results.map((r) => new Track(options?.player, { + title: r.title, + description: r.description, + author: r.channel.name, + url: r.url, + thumbnail: r.thumbnail.displayThumbnailURL(), + duration: r.durationFormatted, + views: r.views, + requestedBy: options?.user, + fromPlaylist: false, + source: "youtube" + }))); + }) + .catch(() => resolve([])); + }); + } +} diff --git a/yarn.lock b/yarn.lock index 2a9d968..ab51860 100644 --- a/yarn.lock +++ b/yarn.lock @@ -143,6 +143,11 @@ balanced-match@^1.0.0: resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c= +boolbase@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e" + integrity sha1-aN/1++YMUes3cl6p4+0xDcwed24= + brace-expansion@^1.1.7: version "1.1.11" resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" @@ -165,6 +170,30 @@ chalk@^2.0.0, chalk@^2.3.0: escape-string-regexp "^1.0.5" supports-color "^5.3.0" +cheerio-select-tmp@^0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/cheerio-select-tmp/-/cheerio-select-tmp-0.1.1.tgz#55bbef02a4771710195ad736d5e346763ca4e646" + integrity sha512-YYs5JvbpU19VYJyj+F7oYrIE2BOll1/hRU7rEy/5+v9BzkSo3bK81iAeeQEMI92vRIxz677m72UmJUiVwwgjfQ== + dependencies: + css-select "^3.1.2" + css-what "^4.0.0" + domelementtype "^2.1.0" + domhandler "^4.0.0" + domutils "^2.4.4" + +cheerio@^1.0.0-rc.3: + version "1.0.0-rc.5" + resolved "https://registry.yarnpkg.com/cheerio/-/cheerio-1.0.0-rc.5.tgz#88907e1828674e8f9fee375188b27dadd4f0fa2f" + integrity sha512-yoqps/VCaZgN4pfXtenwHROTp8NG6/Hlt4Jpz2FEP0ZJQ+ZUkVDd0hAPDNKhj3nakpfPt/CNs57yEtxD1bXQiw== + dependencies: + cheerio-select-tmp "^0.1.0" + dom-serializer "~1.2.0" + domhandler "^4.0.0" + entities "~2.1.0" + htmlparser2 "^6.0.0" + parse5 "^6.0.0" + parse5-htmlparser2-tree-adapter "^6.0.0" + chownr@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/chownr/-/chownr-2.0.0.tgz#15bfbe53d2eab4cf70f18a8cd68ebe5b3cb1dece" @@ -214,6 +243,29 @@ core-util-is@~1.0.0: resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac= +cross-fetch@^3.0.5: + version "3.1.4" + resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-3.1.4.tgz#9723f3a3a247bf8b89039f3a380a9244e8fa2f39" + integrity sha512-1eAtFWdIubi6T4XPy6ei9iUFoKpUkIF971QLN8lIvvvwueI65+Nw5haMNKUwfJxabqlIIDODJKGrQ66gxC0PbQ== + dependencies: + node-fetch "2.6.1" + +css-select@^3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/css-select/-/css-select-3.1.2.tgz#d52cbdc6fee379fba97fb0d3925abbd18af2d9d8" + integrity sha512-qmss1EihSuBNWNNhHjxzxSfJoFBM/lERB/Q4EnsJQQC62R2evJDW481091oAdOr9uh46/0n4nrg0It5cAnj1RA== + dependencies: + boolbase "^1.0.0" + css-what "^4.0.0" + domhandler "^4.0.0" + domutils "^2.4.3" + nth-check "^2.0.0" + +css-what@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/css-what/-/css-what-4.0.0.tgz#35e73761cab2eeb3d3661126b23d7aa0e8432233" + integrity sha512-teijzG7kwYfNVsUh2H/YN62xW3KK9YhXEgSlbxMlcyjPNvdKJqFx5lrwlJgoFP1ZHlB89iGDlo/JyshKeRhv5A== + debug@4: version "4.3.1" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.1.tgz#f0d229c505e0c6d8c49ac553d1b13dc183f6b2ee" @@ -260,6 +312,46 @@ discord.js@discordjs/discord.js: tweetnacl "^1.0.3" ws "^7.3.1" +dom-serializer@^1.0.1, dom-serializer@~1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-1.2.0.tgz#3433d9136aeb3c627981daa385fc7f32d27c48f1" + integrity sha512-n6kZFH/KlCrqs/1GHMOd5i2fd/beQHuehKdWvNNffbGHTr/almdhuVvTVFb3V7fglz+nC50fFusu3lY33h12pA== + dependencies: + domelementtype "^2.0.1" + domhandler "^4.0.0" + entities "^2.0.0" + +domelementtype@^2.0.1, domelementtype@^2.1.0, domelementtype@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.2.0.tgz#9a0b6c2782ed6a1c7323d42267183df9bd8b1d57" + integrity sha512-DtBMo82pv1dFtUmHyr48beiuq792Sxohr+8Hm9zoxklYPfa6n0Z3Byjj2IV7bmr2IyqClnqEQhfgHJJ5QF0R5A== + +domhandler@^4.0.0, domhandler@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-4.1.0.tgz#c1d8d494d5ec6db22de99e46a149c2a4d23ddd43" + integrity sha512-/6/kmsGlMY4Tup/nGVutdrK9yQi4YjWVcVeoQmixpzjOUK1U7pQkvAPHBJeUxOgxF0J8f8lwCJSlCfD0V4CMGQ== + dependencies: + domelementtype "^2.2.0" + +domutils@^2.4.3, domutils@^2.4.4: + version "2.5.1" + resolved "https://registry.yarnpkg.com/domutils/-/domutils-2.5.1.tgz#9b8e84b5d9f788499ae77506ea832e9b4f9aa1c0" + integrity sha512-hO1XwHMGAthA/1KL7c83oip/6UWo3FlUNIuWiWKltoiQ5oCOiqths8KknvY2jpOohUoUgnwa/+Rm7UpwpSbY/Q== + dependencies: + dom-serializer "^1.0.1" + domelementtype "^2.2.0" + domhandler "^4.1.0" + +entities@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/entities/-/entities-2.2.0.tgz#098dc90ebb83d8dffa089d55256b351d34c4da55" + integrity sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A== + +entities@~2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/entities/-/entities-2.1.0.tgz#992d3129cf7df6870b96c57858c249a120f8b8b5" + integrity sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w== + escape-string-regexp@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" @@ -335,6 +427,21 @@ has@^1.0.3: dependencies: function-bind "^1.1.1" +himalaya@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/himalaya/-/himalaya-1.1.0.tgz#31724ae9d35714cd7c6f4be94888953f3604606a" + integrity sha512-LLase1dHCRMel68/HZTFft0N0wti0epHr3nNY7ynpLbyZpmrKMQ8YIpiOV77TM97cNpC8Wb2n6f66IRggwdWPw== + +htmlparser2@^6.0.0: + version "6.0.1" + resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-6.0.1.tgz#422521231ef6d42e56bd411da8ba40aa36e91446" + integrity sha512-GDKPd+vk4jvSuvCbyuzx/unmXkk090Azec7LovXP8as1Hn8q9p3hbjmDGbUqqhknw0ajwit6LiiWqfiTUPMK7w== + dependencies: + domelementtype "^2.0.1" + domhandler "^4.0.0" + domutils "^2.4.4" + entities "^2.0.0" + http-proxy-agent@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz#8a8c8ef7f5932ccf953c296ca8291b95aa74aa3a" @@ -406,7 +513,7 @@ lru-cache@^6.0.0: dependencies: yallist "^4.0.0" -m3u8stream@^0.8.3: +m3u8stream@^0.8.0, m3u8stream@^0.8.3: version "0.8.3" resolved "https://registry.yarnpkg.com/m3u8stream/-/m3u8stream-0.8.3.tgz#c4624e92b4240eb356d040c4a5e155586cf58108" integrity sha512-0nAcdrF8YJKUkb6PzWdvGftTPyCVWgoiot1AkNVbPKTeIGsWs6DrOjifrJ0Zi8WQfQmD2SuVCjkYIOip12igng== @@ -487,7 +594,7 @@ node-addon-api@^3.1.0: resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-3.1.0.tgz#98b21931557466c6729e51cb77cd39c965f42239" integrity sha512-flmrDNB06LIl5lywUz7YlNGZH/5p0M7W28k8hzd9Lshtdh1wshD2Y+U4h9LD6KObOy1f+fEVdgprPrEymjM5uw== -node-fetch@^2.6.0, node-fetch@^2.6.1: +node-fetch@2.6.1, node-fetch@^2.6.0, node-fetch@^2.6.1: version "2.6.1" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.1.tgz#045bd323631f76ed2e2b55573394416b639a0052" integrity sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw== @@ -509,6 +616,13 @@ npmlog@^4.1.2: gauge "~2.7.3" set-blocking "~2.0.0" +nth-check@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-2.0.0.tgz#1bb4f6dac70072fc313e8c9cd1417b5074c0a125" + integrity sha512-i4sc/Kj8htBrAiH1viZ0TgU8Y5XqCaV/FziYK6TBczxmeKm3AEFWqqF3195yKudrarqy7Zu80Ra5dobFjn9X/Q== + dependencies: + boolbase "^1.0.0" + number-is-nan@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d" @@ -526,6 +640,18 @@ once@^1.3.0: dependencies: wrappy "1" +parse5-htmlparser2-tree-adapter@^6.0.0: + version "6.0.1" + resolved "https://registry.yarnpkg.com/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-6.0.1.tgz#2cdf9ad823321140370d4dbf5d3e92c7c8ddc6e6" + integrity sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA== + dependencies: + parse5 "^6.0.1" + +parse5@^6.0.0, parse5@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/parse5/-/parse5-6.0.1.tgz#e1a1c085c569b3dc08321184f19a39cc27f7c30b" + integrity sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw== + path-is-absolute@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" @@ -624,6 +750,29 @@ simple-youtube-api@^5.2.1: iso8601-duration "^1.2.0" node-fetch "^2.6.0" +soundcloud-scraper@^4.0.3: + version "4.0.3" + resolved "https://registry.yarnpkg.com/soundcloud-scraper/-/soundcloud-scraper-4.0.3.tgz#cd7ed1d7b6ed1d7729fd7580c011281f652b920f" + integrity sha512-A0a6sVJ2wkkWIX8Ft3L63sfHBlFDRAaPFif+SWi07KCNLh8YTcylw45pts76pndxlupKwV2NgOTIYeF/F9tg8w== + dependencies: + cheerio "^1.0.0-rc.3" + m3u8stream "^0.8.0" + node-fetch "^2.6.1" + +spotify-uri@^2.1.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/spotify-uri/-/spotify-uri-2.2.0.tgz#8db641615cf6e122284874287fe39e89595922df" + integrity sha512-uUybj02bfyfCoZ0MJ80MkqbKxtIVRJfbRGk05KJFq1li3zb7yNfN1f+TAw4wcXgp7jLWExeiw2wyPQXZ8PHtfg== + +spotify-url-info@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/spotify-url-info/-/spotify-url-info-2.2.0.tgz#7d14adbae65b54b918c46e2dcfdf02b1146f85c8" + integrity sha512-GEMoMf2RF+CSPsSGstY/9c7dgViKOKJ09bFZTwrU4KzQ+JpLq+0Ho4eMCeeGmES94yjBz+GHMtBfTcp+4DxEbA== + dependencies: + cross-fetch "^3.0.5" + himalaya "^1.1.0" + spotify-uri "^2.1.0" + sprintf-js@~1.0.2: version "1.0.3" resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c"