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 { Client, GuildMember, Message, TextChannel } from "discord.js";
import { Player, Queue, Track } from "../src/index"; import { Player, Queue, Track } from "../src/index";
import { QueryType } from "../src/types/types";
import { config } from "./config"; import { config } from "./config";
// use this in prod. // use this in prod.
// import { Player, Queue } from "discord-player"; // 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", name: "volume",
description: "Sets music 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 }); 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(); await interaction.defer();
const query = interaction.options.get("query")!.value! as string; 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!" }); if (!searchResult.length) return void interaction.followUp({ content: "No results were found!" });
const queue = await player.createQueue(interaction.guild, { 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 { TypedEmitter as EventEmitter } from "tiny-typed-emitter";
import { Queue } from "./Structures/Queue"; import { Queue } from "./Structures/Queue";
import { VoiceUtils } from "./VoiceInterface/VoiceUtils"; 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 Track from "./Structures/Track";
import { QueryResolver } from "./utils/QueryResolver"; import { QueryResolver } from "./utils/QueryResolver";
import YouTube from "youtube-sr"; 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> { class DiscordPlayer extends EventEmitter<PlayerEvents> {
public readonly client: Client; public readonly client: Client;
@ -75,11 +80,13 @@ class DiscordPlayer extends EventEmitter<PlayerEvents> {
* @param {Discord.User} requestedBy The person who requested track search * @param {Discord.User} requestedBy The person who requested track search
* @returns {Promise<Track[]>} * @returns {Promise<Track[]>}
*/ */
async search(query: string | Track, requestedBy: User) { async search(query: string | Track, options: SearchOptions) {
if (query instanceof Track) return [query]; 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 // @todo: add extractors
const qt = QueryResolver.resolve(query); const qt = options.searchEngine === QueryType.AUTO ? QueryResolver.resolve(query) : options.searchEngine;
switch (qt) { switch (qt) {
case QueryType.YOUTUBE_SEARCH: { case QueryType.YOUTUBE_SEARCH: {
const videos = await YouTube.search(query, { const videos = await YouTube.search(query, {
@ -93,7 +100,7 @@ class DiscordPlayer extends EventEmitter<PlayerEvents> {
description: m.description, description: m.description,
author: m.channel?.name, author: m.channel?.name,
url: m.url, url: m.url,
requestedBy: requestedBy, requestedBy: options.requestedBy,
thumbnail: m.thumbnail?.displayThumbnailURL("maxresdefault"), thumbnail: m.thumbnail?.displayThumbnailURL("maxresdefault"),
views: m.views, views: m.views,
fromPlaylist: false, 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: default:
return []; return [];
} }

View file

@ -54,8 +54,8 @@ class Queue<T = unknown> {
if (channel.type === "stage") await channel.guild.me.voice.setRequestToSpeak(true).catch(() => {}); 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("error", (err) => this.player.emit("error", this, err));
this.connection.on("debug", msg => this.player.emit("debug", this, msg)); this.connection.on("debug", (msg) => this.player.emit("debug", this, msg));
return this; return this;
} }

View file

@ -158,7 +158,7 @@ class BasicStreamDispatcher extends EventEmitter<VoiceEvents> {
} }
get paused() { 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 { export enum QueryType {
AUTO = "auto",
YOUTUBE = "youtube", YOUTUBE = "youtube",
YOUTUBE_PLAYLIST = "youtube_playlist", YOUTUBE_PLAYLIST = "youtube_playlist",
SOUNDCLOUD_TRACK = "soundcloud_track", SOUNDCLOUD_TRACK = "soundcloud_track",
@ -112,7 +113,8 @@ export enum QueryType {
VIMEO = "vimeo", VIMEO = "vimeo",
ARBITRARY = "arbitrary", ARBITRARY = "arbitrary",
REVERBNATION = "reverbnation", REVERBNATION = "reverbnation",
YOUTUBE_SEARCH = "youtube_search" YOUTUBE_SEARCH = "youtube_search",
SOUNDCLOUD_SEARCH = "soundcloud_search"
} }
export interface PlayerEvents { export interface PlayerEvents {
@ -149,3 +151,8 @@ export interface PlayOptions {
/** If it should start playing provided track immediately */ /** If it should start playing provided track immediately */
immediate?: boolean; 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 };