implement soundcloud search
This commit is contained in:
parent
f38e5a8b11
commit
dac08c1d3f
6 changed files with 105 additions and 11 deletions
|
@ -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, {
|
||||||
|
|
|
@ -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 [];
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
|
@ -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 };
|
||||||
|
|
Loading…
Reference in a new issue