playlists and some methods
This commit is contained in:
parent
224473d63d
commit
610edb9755
3 changed files with 238 additions and 12 deletions
240
src/Player.ts
240
src/Player.ts
|
@ -1,5 +1,5 @@
|
||||||
import { EventEmitter } from 'events';
|
import { EventEmitter } from 'events';
|
||||||
import { Client, Collection, Snowflake, Collector, Message } from 'discord.js';
|
import { Client, Collection, Snowflake, Collector, Message, VoiceChannel } from 'discord.js';
|
||||||
import { PlayerOptions, QueueFilters } from './types/types';
|
import { PlayerOptions, QueueFilters } from './types/types';
|
||||||
import Util from './utils/Util';
|
import Util from './utils/Util';
|
||||||
import AudioFilters from './utils/AudioFilters';
|
import AudioFilters from './utils/AudioFilters';
|
||||||
|
@ -13,6 +13,7 @@ import ytdl from 'discord-ytdl-core';
|
||||||
import spotify from 'spotify-url-info';
|
import spotify from 'spotify-url-info';
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import { Client as SoundCloudClient } from 'soundcloud-scraper';
|
import { Client as SoundCloudClient } from 'soundcloud-scraper';
|
||||||
|
import YouTube from 'youtube-sr';
|
||||||
|
|
||||||
const SoundCloud = new SoundCloudClient();
|
const SoundCloud = new SoundCloudClient();
|
||||||
|
|
||||||
|
@ -99,18 +100,172 @@ export class Player extends EventEmitter {
|
||||||
if (spotifyData) {
|
if (spotifyData) {
|
||||||
tracks = await Util.ytSearch(`${spotifyData.artist} - ${spotifyData.title}`, {
|
tracks = await Util.ytSearch(`${spotifyData.artist} - ${spotifyData.title}`, {
|
||||||
user: message.author,
|
user: message.author,
|
||||||
player: this
|
player: this,
|
||||||
|
limit: 1
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
// todo: make spotify playlist/album load faster
|
||||||
|
case 'spotify_album':
|
||||||
|
case 'spotify_playlist': {
|
||||||
|
this.emit(PlayerEvents.PLAYLIST_PARSE_START, null, message);
|
||||||
|
const playlist = await spotify.getData(query);
|
||||||
|
if (!playlist) return void this.emit(PlayerEvents.NO_RESULTS, message, query);
|
||||||
|
|
||||||
|
// tslint:disable:no-shadowed-variable
|
||||||
|
const tracks = [];
|
||||||
|
|
||||||
|
for (const item of playlist.tracks.items) {
|
||||||
|
const sq =
|
||||||
|
queryType === 'spotify_album'
|
||||||
|
? `${item.artists[0].name} - ${item.name}`
|
||||||
|
: `${item.track.artists[0].name} - ${item.name}`;
|
||||||
|
const data = await Util.ytSearch(sq, {
|
||||||
|
limit: 1,
|
||||||
|
player: this,
|
||||||
|
user: message.author,
|
||||||
|
pl: true
|
||||||
|
});
|
||||||
|
|
||||||
|
if (data[0]) tracks.push(data[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!tracks.length) return void this.emit(PlayerEvents.NO_RESULTS, message, query);
|
||||||
|
|
||||||
|
const pl = {
|
||||||
|
...playlist,
|
||||||
|
tracks,
|
||||||
|
duration: tracks.reduce((a, c) => a + c.durationMS, 0),
|
||||||
|
thumbnail: playlist.images[0]?.url ?? tracks[0].thumbnail
|
||||||
|
};
|
||||||
|
|
||||||
|
this.emit(PlayerEvents.PLAYLIST_PARSE_END, pl, message);
|
||||||
|
|
||||||
|
if (this.isPlaying(message)) {
|
||||||
|
const queue = this._addTracksToQueue(message, tracks);
|
||||||
|
this.emit(PlayerEvents.PLAYLIST_ADD, message, queue, pl);
|
||||||
|
} else {
|
||||||
|
const track = tracks.shift();
|
||||||
|
const queue = (await this._createQueue(message, track).catch(
|
||||||
|
(e) => void this.emit(PlayerEvents.ERROR, e, message)
|
||||||
|
)) as Queue;
|
||||||
|
this.emit(PlayerEvents.TRACK_START, message, queue.tracks[0], queue);
|
||||||
|
this._addTracksToQueue(message, tracks);
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
case 'youtube_playlist': {
|
||||||
|
this.emit(PlayerEvents.PLAYLIST_PARSE_START, null, message);
|
||||||
|
const playlist = await YouTube.getPlaylist(query);
|
||||||
|
if (!playlist) return void this.emit(PlayerEvents.NO_RESULTS, message, query);
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
playlist.videos = playlist.videos.map(
|
||||||
|
(data) =>
|
||||||
|
new Track(this, {
|
||||||
|
title: data.title,
|
||||||
|
url: data.url,
|
||||||
|
duration: Util.durationString(Util.parseMS(data.duration)),
|
||||||
|
description: data.description,
|
||||||
|
thumbnail: data.thumbnail?.displayThumbnailURL(),
|
||||||
|
views: data.views,
|
||||||
|
author: data.channel.name,
|
||||||
|
requestedBy: message.author,
|
||||||
|
fromPlaylist: true,
|
||||||
|
source: 'youtube'
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
playlist.duration = playlist.videos.reduce((a, c) => a + c.durationMS, 0);
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
playlist.thumbnail = playlist.thumbnail?.url ?? playlist.videos[0].thumbnail;
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
playlist.requestedBy = message.author;
|
||||||
|
|
||||||
|
this.emit(PlayerEvents.PLAYLIST_PARSE_END, playlist, message);
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
const tracks = playlist.videos as Track[];
|
||||||
|
|
||||||
|
if (this.isPlaying(message)) {
|
||||||
|
const queue = this._addTracksToQueue(message, tracks);
|
||||||
|
this.emit(PlayerEvents.PLAYLIST_ADD, message, queue, playlist);
|
||||||
|
} else {
|
||||||
|
const track = tracks.shift();
|
||||||
|
const queue = (await this._createQueue(message, track).catch(
|
||||||
|
(e) => void this.emit(PlayerEvents.ERROR, e, message)
|
||||||
|
)) as Queue;
|
||||||
|
this.emit(PlayerEvents.TRACK_START, message, queue.tracks[0], queue);
|
||||||
|
this._addTracksToQueue(message, tracks);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case 'soundcloud_playlist': {
|
||||||
|
this.emit(PlayerEvents.PLAYLIST_PARSE_START, null, message);
|
||||||
|
|
||||||
|
const data = await SoundCloud.getPlaylist(query).catch(() => {});
|
||||||
|
if (!data) return void this.emit(PlayerEvents.NO_RESULTS, message, query);
|
||||||
|
|
||||||
|
const res = {
|
||||||
|
id: data.id,
|
||||||
|
title: data.title,
|
||||||
|
tracks: [] as Track[],
|
||||||
|
author: data.author,
|
||||||
|
duration: 0,
|
||||||
|
thumbnail: data.thumbnail,
|
||||||
|
requestedBy: message.author
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const song of data.tracks) {
|
||||||
|
const r = new Track(this, {
|
||||||
|
title: song.title,
|
||||||
|
url: song.url,
|
||||||
|
duration: Util.durationString(Util.parseMS(song.duration / 1000)),
|
||||||
|
description: song.description,
|
||||||
|
thumbnail: song.thumbnail ?? 'https://soundcloud.com/pwa-icon-192.png',
|
||||||
|
views: song.playCount ?? 0,
|
||||||
|
author: song.author ?? data.author,
|
||||||
|
requestedBy: message.author,
|
||||||
|
fromPlaylist: true,
|
||||||
|
source: 'soundcloud',
|
||||||
|
engine: song
|
||||||
|
});
|
||||||
|
|
||||||
|
res.tracks.push(r);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!res.tracks.length)
|
||||||
|
return this.emit(PlayerEvents.ERROR, PlayerErrorEventCodes.PARSE_ERROR, message);
|
||||||
|
res.duration = res.tracks.reduce((a, c) => a + c.durationMS, 0);
|
||||||
|
|
||||||
|
this.emit(PlayerEvents.PLAYLIST_PARSE_END, res, message);
|
||||||
|
|
||||||
|
if (this.isPlaying(message)) {
|
||||||
|
const queue = this._addTracksToQueue(message, res.tracks);
|
||||||
|
this.emit(PlayerEvents.PLAYLIST_ADD, message, queue, res);
|
||||||
|
} else {
|
||||||
|
const track = res.tracks.shift();
|
||||||
|
const queue = (await this._createQueue(message, track).catch(
|
||||||
|
(e) => void this.emit(PlayerEvents.ERROR, e, message)
|
||||||
|
)) as Queue;
|
||||||
|
this.emit(PlayerEvents.TRACK_START, message, queue.tracks[0], queue);
|
||||||
|
this._addTracksToQueue(message, res.tracks);
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
tracks = await Util.ytSearch(query, { user: message.author, player: this });
|
tracks = await Util.ytSearch(query, { user: message.author, player: this });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tracks.length < 1) return this.emit(PlayerEvents.NO_RESULTS, message, query);
|
if (tracks.length < 1) return void this.emit(PlayerEvents.NO_RESULTS, message, query);
|
||||||
if (firstResult) return resolve(tracks[0]);
|
if (firstResult || tracks.length === 1) return resolve(tracks[0]);
|
||||||
|
|
||||||
const collectorString = `${message.author.id}-${message.channel.id}`;
|
const collectorString = `${message.author.id}-${message.channel.id}`;
|
||||||
const currentCollector = this._resultsCollectors.get(collectorString);
|
const currentCollector = this._resultsCollectors.get(collectorString);
|
||||||
|
@ -140,7 +295,7 @@ export class Player extends EventEmitter {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
collector.on('end', (collected, reason) => {
|
collector.on('end', (_, reason) => {
|
||||||
if (reason === 'time') {
|
if (reason === 'time') {
|
||||||
this.emit(PlayerEvents.SEARCH_CANCEL, message, query, tracks);
|
this.emit(PlayerEvents.SEARCH_CANCEL, message, query, tracks);
|
||||||
}
|
}
|
||||||
|
@ -232,6 +387,67 @@ export class Player extends EventEmitter {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setPosition(message: Message, time: number): Promise<void> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const queue = this.queues.find((g) => g.guildID === message.guild.id);
|
||||||
|
if (!queue) return this.emit('error', 'NotPlaying', message);
|
||||||
|
|
||||||
|
if (typeof time !== 'number' && !isNaN(time)) time = parseInt(time);
|
||||||
|
if (queue.playing.durationMS >= time) return this.skip(message);
|
||||||
|
if (
|
||||||
|
queue.voiceConnection.dispatcher.streamTime === time ||
|
||||||
|
queue.voiceConnection.dispatcher.streamTime + queue.additionalStreamTime === time
|
||||||
|
)
|
||||||
|
return resolve();
|
||||||
|
if (time < 0) this._playStream(queue, false).then(() => resolve());
|
||||||
|
|
||||||
|
this._playStream(queue, false, time).then(() => resolve());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
seek(message: Message, time: number) {
|
||||||
|
return this.setPosition(message, time);
|
||||||
|
}
|
||||||
|
|
||||||
|
skip(message: Message): boolean {
|
||||||
|
const queue = this.getQueue(message);
|
||||||
|
if (!queue) {
|
||||||
|
this.emit(PlayerEvents.ERROR, PlayerErrorEventCodes.NOT_PLAYING, message);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!queue.voiceConnection || !queue.voiceConnection.dispatcher) {
|
||||||
|
this.emit(PlayerEvents.ERROR, PlayerErrorEventCodes.MUSIC_STARTING, message);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
queue.voiceConnection.dispatcher.end();
|
||||||
|
queue.lastSkipped = true;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
moveTo(message: Message, channel?: VoiceChannel) {
|
||||||
|
if (!channel || channel.type !== 'voice') return;
|
||||||
|
const queue = this.queues.find((g) => g.guildID === message.guild.id);
|
||||||
|
if (!queue) {
|
||||||
|
this.emit(PlayerEvents.ERROR, PlayerErrorEventCodes.NOT_PLAYING, message);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!queue.voiceConnection || !queue.voiceConnection.dispatcher) {
|
||||||
|
this.emit(PlayerEvents.ERROR, PlayerErrorEventCodes.MUSIC_STARTING, message);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (queue.voiceConnection.channel.id === channel.id) return;
|
||||||
|
|
||||||
|
queue.voiceConnection.dispatcher.pause();
|
||||||
|
channel
|
||||||
|
.join()
|
||||||
|
.then(() => queue.voiceConnection.dispatcher.resume())
|
||||||
|
.catch(() => this.emit(PlayerEvents.ERROR, PlayerErrorEventCodes.UNABLE_TO_JOIN, message));
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
private _addTrackToQueue(message: Message, track: Track) {
|
private _addTrackToQueue(message: Message, track: Track) {
|
||||||
const queue = this.getQueue(message);
|
const queue = this.getQueue(message);
|
||||||
if (!queue)
|
if (!queue)
|
||||||
|
@ -246,11 +462,21 @@ export class Player extends EventEmitter {
|
||||||
return queue;
|
return queue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _addTracksToQueue(message: Message, tracks: Track[]) {
|
||||||
|
const queue = this.getQueue(message);
|
||||||
|
if (!queue)
|
||||||
|
throw new PlayerError(
|
||||||
|
'Cannot add tracks to queue because no song is currently being played on the server.'
|
||||||
|
);
|
||||||
|
queue.tracks.push(...tracks);
|
||||||
|
return queue;
|
||||||
|
}
|
||||||
|
|
||||||
private _createQueue(message: Message, track: Track): Promise<Queue> {
|
private _createQueue(message: Message, track: Track): Promise<Queue> {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
const channel = message.member.voice ? message.member.voice.channel : null;
|
const channel = message.member.voice ? message.member.voice.channel : null;
|
||||||
if (!channel)
|
if (!channel)
|
||||||
return this.emit(
|
return void this.emit(
|
||||||
PlayerEvents.ERROR,
|
PlayerEvents.ERROR,
|
||||||
PlayerErrorEventCodes.NOT_CONNECTED,
|
PlayerErrorEventCodes.NOT_CONNECTED,
|
||||||
message,
|
message,
|
||||||
|
@ -333,8 +559,6 @@ export class Player extends EventEmitter {
|
||||||
const encoderArgsFilters: string[] = [];
|
const encoderArgsFilters: string[] = [];
|
||||||
|
|
||||||
Object.keys(queue.filters).forEach((filterName) => {
|
Object.keys(queue.filters).forEach((filterName) => {
|
||||||
if (this.options.enableLive && ["nightcore", "vaporwave", "reverse"].includes(filterName)) return;
|
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
if (queue.filters[filterName]) {
|
if (queue.filters[filterName]) {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
|
|
|
@ -23,5 +23,6 @@ export const PlayerErrorEventCodes = {
|
||||||
UNABLE_TO_JOIN: 'UnableToJoin',
|
UNABLE_TO_JOIN: 'UnableToJoin',
|
||||||
NOT_PLAYING: 'NotPlaying',
|
NOT_PLAYING: 'NotPlaying',
|
||||||
PARSE_ERROR: 'ParseError',
|
PARSE_ERROR: 'ParseError',
|
||||||
VIDEO_UNAVAILABLE: 'VideoUnavailable'
|
VIDEO_UNAVAILABLE: 'VideoUnavailable',
|
||||||
|
MUSIC_STARTING: 'MusicStarting'
|
||||||
};
|
};
|
||||||
|
|
|
@ -54,8 +54,8 @@ export class Util {
|
||||||
if (spotifySongRegex.test(query)) return 'spotify_song';
|
if (spotifySongRegex.test(query)) return 'spotify_song';
|
||||||
if (spotifyAlbumRegex.test(query)) return 'spotify_album';
|
if (spotifyAlbumRegex.test(query)) return 'spotify_album';
|
||||||
if (spotifyPlaylistRegex.test(query)) return 'spotify_playlist';
|
if (spotifyPlaylistRegex.test(query)) return 'spotify_playlist';
|
||||||
if (YouTube.validate(query, 'VIDEO')) return 'youtube_video';
|
|
||||||
if (YouTube.validate(query, 'PLAYLIST')) return 'youtube_playlist';
|
if (YouTube.validate(query, 'PLAYLIST')) return 'youtube_playlist';
|
||||||
|
if (YouTube.validate(query, 'VIDEO')) return 'youtube_video';
|
||||||
if (vimeoRegex.test(query)) return 'vimeo';
|
if (vimeoRegex.test(query)) return 'vimeo';
|
||||||
if (facebookRegex.test(query)) return 'facebook';
|
if (facebookRegex.test(query)) return 'facebook';
|
||||||
if (reverbnationRegex.test(query)) return 'reverbnation';
|
if (reverbnationRegex.test(query)) return 'reverbnation';
|
||||||
|
@ -102,7 +102,8 @@ export class Util {
|
||||||
return new Promise(async (resolve) => {
|
return new Promise(async (resolve) => {
|
||||||
await YouTube.search(query, {
|
await YouTube.search(query, {
|
||||||
type: 'video',
|
type: 'video',
|
||||||
safeSearch: Boolean(options?.player.options.useSafeSearch)
|
safeSearch: Boolean(options?.player.options.useSafeSearch),
|
||||||
|
limit: options.limit ?? 10
|
||||||
})
|
})
|
||||||
.then((results) => {
|
.then((results) => {
|
||||||
resolve(
|
resolve(
|
||||||
|
@ -117,7 +118,7 @@ export class Util {
|
||||||
duration: r.durationFormatted,
|
duration: r.durationFormatted,
|
||||||
views: r.views,
|
views: r.views,
|
||||||
requestedBy: options?.user,
|
requestedBy: options?.user,
|
||||||
fromPlaylist: false,
|
fromPlaylist: Boolean(options?.pl),
|
||||||
source: 'youtube'
|
source: 'youtube'
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
Loading…
Reference in a new issue