Support Spotify track, playlist, and album

This commit is contained in:
2022-07-01 22:04:37 +07:00
parent 929ef256ab
commit 4c1293b319
+187 -8
View File
@@ -1,7 +1,6 @@
import { import {
VoiceChannel, VoiceChannel,
Snowflake, Snowflake,
TextChannel,
StageChannel, StageChannel,
Guild, Guild,
PermissionsBitField, PermissionsBitField,
@@ -16,27 +15,91 @@ import {
createAudioResource, createAudioResource,
VoiceConnectionStatus, VoiceConnectionStatus,
AudioPlayerStatus, AudioPlayerStatus,
AudioPlayerState,
NoSubscriberBehavior, NoSubscriberBehavior,
VoiceConnectionState, VoiceConnectionState,
AudioPlayerError, AudioPlayerError,
DiscordGatewayAdapterCreator DiscordGatewayAdapterCreator
} from '@discordjs/voice'; } from '@discordjs/voice';
import playdl, { YouTubeVideo } from 'play-dl'; import playdl, { Spotify, SpotifyPlaylist, SpotifyTrack, YouTubeVideo } from 'play-dl';
import { EventEmitter } from 'stream'; import { EventEmitter } from 'stream';
import DiscordProvider from './Discord'; import DiscordProvider from './Discord';
import Environment from './Environment'; import Environment from './Environment';
export type ValidTracks = YouTubeVideo; export type ValidTracks = YouTubeVideo | SpotifyTrack;
declare class YouTubeThumbnail {
url: string;
width: number;
height: number;
constructor(data: any);
toJSON(): {
url: string;
width: number;
height: number;
};
}
interface SpotifyThumbnail {
height: number;
width: number;
url: string;
}
if (Environment.get().YOUTUBE_COOKIE_BASE64) { if (Environment.get().YOUTUBE_COOKIE_BASE64) {
playdl.setToken({ playdl.setToken({
youtube: { youtube: {
cookie: Buffer.from(Environment.get().YOUTUBE_COOKIE_BASE64, 'base64').toString() cookie: Buffer.from(Environment.get().YOUTUBE_COOKIE_BASE64, 'base64').toString()
},
spotify: {
client_id: Environment.get().SPOTIFY_CLIENT_ID,
client_secret: Environment.get().SPOTIFY_CLIENT_SECRET,
refresh_token: Environment.get().SPOTIFY_REFRESH_TOKEN,
market: Environment.get().SPOTIFY_CLIENT_MARKET,
} }
}); });
} }
export class TrackUtils {
public static getTitle(track: ValidTracks) {
if (track instanceof YouTubeVideo) {
return track.title;
} else if (track instanceof SpotifyTrack) {
let artistNames = "";
for(let artist of track.artists)
artistNames += artist.name + " ";
return `${artistNames} - ${track.name}`;;
} else {
throw new Error('Invalid track type');
}
}
public static async getThumbnails(track: ValidTracks) {
if (track instanceof YouTubeVideo) {
return track.thumbnails;
} else if (track instanceof SpotifyTrack) {
if (track.thumbnail)
return [track.thumbnail];
else {
// Try to find the thumbnail again
const result = await DiscordMusicPlayer_Instance.searchSpotifyBySpotifyLink(track);
if(!result) return null;
if(result.thumbnail)
return [result.thumbnail];
return null;
}
} else {
throw new Error('Invalid track type');
}
}
public static getHighestResolutionThumbnail(thumbnails: (YouTubeThumbnail[] | SpotifyThumbnail[] | null)) {
if(!thumbnails) return null;
return (thumbnails as any[]).reduce((prev: any, current: any) =>
prev.height * prev.width > current.height * current.width ? prev : current
);
}
}
export class Queue { export class Queue {
public track: ValidTracks[] = []; public track: ValidTracks[] = [];
} }
@@ -51,6 +114,18 @@ export class YouTubeLink {
} }
} }
export class SpotifyLink {
public id: string;
public type: 'track' | 'playlist' | 'album';
public url: string;
constructor(id: string, type: ('track' | 'playlist' | 'album'), url: string) {
this.id = id;
this.type = type;
this.url = url;
}
}
export class PlayerPlayingEvent { export class PlayerPlayingEvent {
public instance: DiscordMusicPlayerInstance; public instance: DiscordMusicPlayerInstance;
@@ -92,6 +167,8 @@ export class DiscordMusicPlayerInstance {
public paused: boolean = false; public paused: boolean = false;
public loopMode: DiscordMusicPlayerLoopMode = DiscordMusicPlayerLoopMode.None; public loopMode: DiscordMusicPlayerLoopMode = DiscordMusicPlayerLoopMode.None;
public actualPlaybackURL?: string;
public readonly events: EventEmitter; public readonly events: EventEmitter;
constructor({ voiceChannel }: { voiceChannel: VoiceChannel | StageChannel }) { constructor({ voiceChannel }: { voiceChannel: VoiceChannel | StageChannel }) {
@@ -229,11 +306,26 @@ export class DiscordMusicPlayerInstance {
if (!this.voiceConnection) throw new Error('No voice connection'); if (!this.voiceConnection) throw new Error('No voice connection');
try { try {
const stream = await playdl.stream(track.url);
const resource = createAudioResource(stream.stream, {
inputType: stream.type
});
let resource;
if(track instanceof YouTubeVideo) {
const stream = await playdl.stream(track.url);
resource = createAudioResource(stream.stream, {
inputType: stream.type
});
this.actualPlaybackURL = track.url;
} else {
const search = await DiscordMusicPlayer_Instance.searchYouTubeBySpotifyLink(track);
if(!search)
throw new Error('Unable to find Spotify track on YouTube');
const stream = await playdl.stream(search.url);
resource = createAudioResource(stream.stream, {
inputType: stream.type
});
this.actualPlaybackURL = search.url;
}
this.player.play(resource); this.player.play(resource);
this.voiceConnection.subscribe(this.player); this.voiceConnection.subscribe(this.player);
} catch (error: any) { } catch (error: any) {
@@ -268,6 +360,10 @@ export class DiscordMusicPlayerInstance {
return this.previousTrack; return this.previousTrack;
} }
public getActualPlaybackURL() {
return this.actualPlaybackURL;
}
public isPaused() { public isPaused() {
return this.paused; return this.paused;
} }
@@ -331,6 +427,41 @@ class DiscordMusicPlayer {
return searched; return searched;
} }
public async searchYouTubeBySpotifyLink(spotifyLink: SpotifyLink) {
const track = await this.searchSpotifyBySpotifyLink(spotifyLink);
if(!track) return;
let artistNames = "";
for(let artist of track.artists)
artistNames += artist.name + " ";
const ytSearchResult = await this.searchYouTubeByQuery(`${artistNames} ${track.name}`);
if(!ytSearchResult) return null;
return ytSearchResult[0];
}
public async searchSpotifyBySpotifyLink(spotifyLink: SpotifyLink) {
if(playdl.is_expired())
await playdl.refreshToken();
if(spotifyLink.type != 'track') return;
if(playdl.is_expired())
await playdl.refreshToken();
if(spotifyLink.type != 'track') return;
// Fetch data from spotify
const searched: Spotify = await playdl.spotify("https://open.spotify.com/track/" + spotifyLink.id);
if(!(searched instanceof SpotifyTrack)) return;
if (!searched) return null;
const track = searched as unknown as SpotifyTrack;
return track;
}
public async searchYouTubeByYouTubeLink(youtubeLink: YouTubeLink) { public async searchYouTubeByYouTubeLink(youtubeLink: YouTubeLink) {
// Search the url // Search the url
const searched: YouTubeVideo[] = await playdl.search('https://www.youtube.com/watch?v=' + youtubeLink.videoId, { const searched: YouTubeVideo[] = await playdl.search('https://www.youtube.com/watch?v=' + youtubeLink.videoId, {
@@ -393,6 +524,15 @@ class DiscordMusicPlayer {
}); });
} }
public async getSpotifySongsInPlayList(spotifyLink: string) {
const result = await playdl.spotify(spotifyLink);
if(!(result.type == 'playlist' || result.type == 'album'))
throw new Error("Not a spotify playlist");
return result as unknown as SpotifyPlaylist;
}
public isYouTubeLink(link: string): boolean { public isYouTubeLink(link: string): boolean {
try { try {
this.parseYouTubeLink(link); this.parseYouTubeLink(link);
@@ -402,6 +542,15 @@ class DiscordMusicPlayer {
} }
} }
public isSpotifyLink(link: string): boolean {
try {
this.parseSpotifyLink(link);
return true;
} catch (err) {
return false;
}
}
public parseYouTubeLink(query: string): YouTubeLink { public parseYouTubeLink(query: string): YouTubeLink {
if ( if (
query.startsWith('https://www.youtube.com/watch?v=') || query.startsWith('https://www.youtube.com/watch?v=') ||
@@ -437,6 +586,36 @@ class DiscordMusicPlayer {
} }
} }
public parseSpotifyLink(query: string): SpotifyLink {
if (query.startsWith('https://open.spotify.com/track/')) {
let id = query.split('/')[4].split(/[?#]/)[0];
return {
id: id,
type: 'track',
url: query.split(/[?#]/)[0]
};
}
else if (query.startsWith('https://open.spotify.com/album/')) {
let id = query.split('/')[4].split(/[?#]/)[0];
return {
id: id,
type: 'album',
url: query.split(/[?#]/)[0]
};
}
else if (query.startsWith('https://open.spotify.com/playlist/')) {
let id = query.split('/')[4].split(/[?#]/)[0];
return {
id: id,
type: 'playlist',
url: query.split(/[?#]/)[0]
};
}
else {
throw new Error('Spotify link is invalid');
}
}
private parseURLQuery(query: string) { private parseURLQuery(query: string) {
let queryObject: any = {}; let queryObject: any = {};
if (query.indexOf('?') >= 0) { if (query.indexOf('?') >= 0) {