implement soundcloud search

This commit is contained in:
Snowflake107 2021-06-13 17:48:20 +05:45
parent f38e5a8b11
commit dac08c1d3f
6 changed files with 105 additions and 11 deletions

View file

@ -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, {

View file

@ -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<PlayerEvents> {
public readonly client: Client;
@ -75,11 +80,13 @@ class DiscordPlayer extends EventEmitter<PlayerEvents> {
* @param {Discord.User} requestedBy The person who requested track search
* @returns {Promise<Track[]>}
*/
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<PlayerEvents> {
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<PlayerEvents> {
});
});
}
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 [];
}

View file

@ -54,8 +54,8 @@ class Queue<T = unknown> {
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;
}

View file

@ -158,7 +158,7 @@ class BasicStreamDispatcher extends EventEmitter<VoiceEvents> {
}
get paused() {
return [AudioPlayerStatus.AutoPaused, AudioPlayerStatus.Paused].includes(this.audioPlayer.state.status)
return [AudioPlayerStatus.AutoPaused, AudioPlayerStatus.Paused].includes(this.audioPlayer.state.status);
}
}

View file

@ -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;
}

View file

@ -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 };