diff --git a/example/index.ts b/example/index.ts index e3b90e4..da746bc 100644 --- a/example/index.ts +++ b/example/index.ts @@ -1,5 +1,6 @@ import { Client, GuildMember, Message, TextChannel } from "discord.js"; import { Player, Queue, Track } from "../src/index"; +import { QueryType } from "../src/types/types"; import { config } from "./config"; // use this in prod. // import { Player, Queue } from "discord-player"; @@ -51,6 +52,18 @@ client.on("message", async (message) => { } ] }, + { + name: "soundcloud", + description: "Plays a song from soundcloud", + options: [ + { + name: "query", + type: "STRING", + description: "The song you want to play", + required: true + } + ] + }, { name: "volume", description: "Sets music volume", @@ -104,11 +117,16 @@ client.on("interaction", async (interaction) => { return void interaction.reply({ content: "You are not in my voice channel!", ephemeral: true }); } - if (interaction.commandName === "play") { + if (interaction.commandName === "play" || interaction.commandName === "soundcloud") { await interaction.defer(); const query = interaction.options.get("query")!.value! as string; - const searchResult = (await player.search(query, interaction.user).catch(() => [])) as Track[]; + const searchResult = (await player + .search(query, { + requestedBy: interaction.user, + searchEngine: interaction.commandName === "soundcloud" ? QueryType.SOUNDCLOUD_SEARCH : QueryType.AUTO + }) + .catch(() => [])) as Track[]; if (!searchResult.length) return void interaction.followUp({ content: "No results were found!" }); const queue = await player.createQueue(interaction.guild, { diff --git a/src/Player.ts b/src/Player.ts index d1babe1..df299ce 100644 --- a/src/Player.ts +++ b/src/Player.ts @@ -2,10 +2,15 @@ import { Client, Collection, Guild, Snowflake, User } from "discord.js"; import { TypedEmitter as EventEmitter } from "tiny-typed-emitter"; import { Queue } from "./Structures/Queue"; import { VoiceUtils } from "./VoiceInterface/VoiceUtils"; -import { PlayerEvents, PlayerOptions, QueryType } from "./types/types"; +import { PlayerEvents, PlayerOptions, QueryType, SearchOptions } from "./types/types"; import Track from "./Structures/Track"; import { QueryResolver } from "./utils/QueryResolver"; import YouTube from "youtube-sr"; +import { Util } from "./utils/Util"; +// @ts-ignore +import { Client as SoundCloud } from "soundcloud-scraper"; + +const soundcloud = new SoundCloud(); class DiscordPlayer extends EventEmitter { public readonly client: Client; @@ -75,11 +80,13 @@ class DiscordPlayer extends EventEmitter { * @param {Discord.User} requestedBy The person who requested track search * @returns {Promise} */ - async search(query: string | Track, requestedBy: User) { + async search(query: string | Track, options: SearchOptions) { if (query instanceof Track) return [query]; + if (!options) throw new Error("DiscordPlayer#search needs search options!"); + if (!("searchEngine" in options)) options.searchEngine = QueryType.AUTO; // @todo: add extractors - const qt = QueryResolver.resolve(query); + const qt = options.searchEngine === QueryType.AUTO ? QueryResolver.resolve(query) : options.searchEngine; switch (qt) { case QueryType.YOUTUBE_SEARCH: { const videos = await YouTube.search(query, { @@ -93,7 +100,7 @@ class DiscordPlayer extends EventEmitter { description: m.description, author: m.channel?.name, url: m.url, - requestedBy: requestedBy, + requestedBy: options.requestedBy, thumbnail: m.thumbnail?.displayThumbnailURL("maxresdefault"), views: m.views, fromPlaylist: false, @@ -102,6 +109,35 @@ class DiscordPlayer extends EventEmitter { }); }); } + case QueryType.SOUNDCLOUD_TRACK: + case QueryType.SOUNDCLOUD_SEARCH: { + const result: any[] = QueryResolver.resolve(query) === QueryType.SOUNDCLOUD_TRACK ? [{ url: query }] : await soundcloud.search(query, "track").catch(() => {}); + if (!result || !result.length) return []; + const res: Track[] = []; + + for (const r of result) { + const trackInfo = await soundcloud.getSongInfo(r.url).catch(() => {}); + if (!trackInfo) continue; + + const track = new Track(this, { + title: trackInfo.title, + url: trackInfo.url, + duration: Util.buildTimeCode(Util.parseMS(trackInfo.duration)), + description: trackInfo.description, + thumbnail: trackInfo.thumbnail, + views: trackInfo.playCount, + author: trackInfo.author.name, + requestedBy: options.requestedBy, + fromPlaylist: false, + source: "soundcloud", + engine: trackInfo + }); + + res.push(track); + } + + return res; + } default: return []; } diff --git a/src/Structures/Queue.ts b/src/Structures/Queue.ts index 072d9fe..604f9c7 100644 --- a/src/Structures/Queue.ts +++ b/src/Structures/Queue.ts @@ -54,8 +54,8 @@ class Queue { if (channel.type === "stage") await channel.guild.me.voice.setRequestToSpeak(true).catch(() => {}); - this.connection.on("error", err => this.player.emit("error", this, err)); - this.connection.on("debug", msg => this.player.emit("debug", this, msg)); + this.connection.on("error", (err) => this.player.emit("error", this, err)); + this.connection.on("debug", (msg) => this.player.emit("debug", this, msg)); return this; } diff --git a/src/VoiceInterface/BasicStreamDispatcher.ts b/src/VoiceInterface/BasicStreamDispatcher.ts index 91301be..7579ada 100644 --- a/src/VoiceInterface/BasicStreamDispatcher.ts +++ b/src/VoiceInterface/BasicStreamDispatcher.ts @@ -158,7 +158,7 @@ class BasicStreamDispatcher extends EventEmitter { } get paused() { - return [AudioPlayerStatus.AutoPaused, AudioPlayerStatus.Paused].includes(this.audioPlayer.state.status) + return [AudioPlayerStatus.AutoPaused, AudioPlayerStatus.Paused].includes(this.audioPlayer.state.status); } } diff --git a/src/types/types.ts b/src/types/types.ts index 3e06853..4a118ac 100644 --- a/src/types/types.ts +++ b/src/types/types.ts @@ -100,6 +100,7 @@ export interface ExtractorModelData { } export enum QueryType { + AUTO = "auto", YOUTUBE = "youtube", YOUTUBE_PLAYLIST = "youtube_playlist", SOUNDCLOUD_TRACK = "soundcloud_track", @@ -112,7 +113,8 @@ export enum QueryType { VIMEO = "vimeo", ARBITRARY = "arbitrary", REVERBNATION = "reverbnation", - YOUTUBE_SEARCH = "youtube_search" + YOUTUBE_SEARCH = "youtube_search", + SOUNDCLOUD_SEARCH = "soundcloud_search" } export interface PlayerEvents { @@ -149,3 +151,8 @@ export interface PlayOptions { /** If it should start playing provided track immediately */ immediate?: boolean; } + +export interface SearchOptions { + requestedBy: User; + searchEngine?: QueryType; +} diff --git a/src/utils/Util.ts b/src/utils/Util.ts index cb0ff5c..8d54958 100644 --- a/src/utils/Util.ts +++ b/src/utils/Util.ts @@ -1 +1,34 @@ -export {}; +import { TimeData } from "../types/types"; + +class Util { + static durationString(durObj: object) { + return Object.values(durObj) + .map((m) => (isNaN(m) ? 0 : m)) + .join(":"); + } + + static parseMS(milliseconds: number) { + const round = milliseconds > 0 ? Math.floor : Math.ceil; + + return { + days: round(milliseconds / 86400000), + hours: round(milliseconds / 3600000) % 24, + minutes: round(milliseconds / 60000) % 60, + seconds: round(milliseconds / 1000) % 60 + } as TimeData; + } + + static buildTimeCode(duration: TimeData) { + const items = Object.keys(duration); + const required = ["days", "hours", "minutes", "seconds"]; + + const parsed = items.filter((x) => required.includes(x)).map((m) => (duration[m as keyof TimeData] > 0 ? duration[m as keyof TimeData] : "")); + const final = parsed + .filter((x) => !!x) + .map((x) => x.toString().padStart(2, "0")) + .join(":"); + return final.length <= 3 ? `0:${final.padStart(2, "0") || 0}` : final; + } +} + +export { Util };