From da5f4a64bab7a6788eb386f77f5a824288d17ddf Mon Sep 17 00:00:00 2001 From: Snowflake107 Date: Sun, 13 Jun 2021 20:16:27 +0545 Subject: [PATCH] partial playlist implementation --- example/index.ts | 9 ++-- src/Player.ts | 87 +++++++++++++++++++++++++++++++++----- src/Structures/Playlist.ts | 42 +++++++++++++++++- src/Structures/Track.ts | 13 +++--- src/types/types.ts | 48 ++++++++++++++++++++- 5 files changed, 174 insertions(+), 25 deletions(-) diff --git a/example/index.ts b/example/index.ts index 6538b64..721ede0 100644 --- a/example/index.ts +++ b/example/index.ts @@ -147,13 +147,13 @@ client.on("interaction", async (interaction) => { await interaction.defer(); const query = interaction.options.get("query")!.value! as string; - const searchResult = (await player + 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!" }); + .catch(() => {}); + if (!searchResult || !searchResult.tracks.length) return void interaction.followUp({ content: "No results were found!" }); const queue = await player.createQueue(interaction.guild, { metadata: interaction.channel as TextChannel @@ -167,7 +167,8 @@ client.on("interaction", async (interaction) => { } await interaction.followUp({ content: "⏱ | Loading your track..." }); - await queue.play(searchResult[0]); + searchResult.playlist ? queue.addTracks(searchResult.tracks) : queue.addTrack(searchResult.tracks[0]); + if (!queue.playing) await queue.play(); } else if (interaction.commandName === "volume") { await interaction.defer(); const queue = player.getQueue(interaction.guildID); diff --git a/src/Player.ts b/src/Player.ts index c5f7d28..c0a3fb0 100644 --- a/src/Player.ts +++ b/src/Player.ts @@ -10,6 +10,7 @@ import { Util } from "./utils/Util"; import Spotify from "spotify-url-info"; // @ts-ignore import { Client as SoundCloud } from "soundcloud-scraper"; +import { Playlist } from "./Structures/Playlist"; const soundcloud = new SoundCloud(); @@ -82,7 +83,7 @@ class DiscordPlayer extends EventEmitter { * @returns {Promise} */ async search(query: string | Track, options: SearchOptions) { - if (query instanceof Track) return [query]; + if (query instanceof Track) return { playlist: false, tracks: [query] }; if (!options) throw new Error("DiscordPlayer#search needs search options!"); if (!("searchEngine" in options)) options.searchEngine = QueryType.AUTO; @@ -92,9 +93,10 @@ class DiscordPlayer extends EventEmitter { case QueryType.YOUTUBE_SEARCH: { const videos = await YouTube.search(query, { type: "video" - }); + }).catch(() => {}); + if (!videos) return { playlist: false, tracks: [] }; - return videos.map((m) => { + const tracks = videos.map((m) => { (m as any).source = "youtube"; return new Track(this, { title: m.title, @@ -104,16 +106,17 @@ class DiscordPlayer extends EventEmitter { requestedBy: options.requestedBy, thumbnail: m.thumbnail?.displayThumbnailURL("maxresdefault"), views: m.views, - fromPlaylist: false, duration: m.durationFormatted, raw: m }); }); + + return { playlist: false, tracks }; } 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 []; + if (!result || !result.length) return { playlist: false, tracks: [] }; const res: Track[] = []; for (const r of result) { @@ -129,7 +132,6 @@ class DiscordPlayer extends EventEmitter { views: trackInfo.playCount, author: trackInfo.author.name, requestedBy: options.requestedBy, - fromPlaylist: false, source: "soundcloud", engine: trackInfo }); @@ -137,11 +139,11 @@ class DiscordPlayer extends EventEmitter { res.push(track); } - return res; + return { playlist: false, tracks: res }; } case QueryType.SPOTIFY_SONG: { const spotifyData = await Spotify.getData(query).catch(() => {}); - if (!spotifyData) return []; + if (!spotifyData) return { playlist: false, tracks: [] }; const spotifyTrack = new Track(this, { title: spotifyData.name, description: spotifyData.description ?? "", @@ -154,14 +156,77 @@ class DiscordPlayer extends EventEmitter { duration: Util.buildTimeCode(Util.parseMS(spotifyData.duration_ms)), views: 0, requestedBy: options.requestedBy, - fromPlaylist: false, source: "spotify" }); - return [spotifyTrack]; + return { playlist: false, tracks: [spotifyTrack] }; + } + case QueryType.SPOTIFY_PLAYLIST: + case QueryType.SPOTIFY_ALBUM: { + const spotifyPlaylist = await Spotify.getData(query).catch(() => {}); + if (!spotifyPlaylist) return { playlist: false, tracks: [] }; + + const playlist = new Playlist(this, { + title: spotifyPlaylist.name ?? spotifyPlaylist.title, + description: spotifyPlaylist.description ?? "", + thumbnail: spotifyPlaylist.images[0]?.url ?? "https://www.scdn.co/i/_global/twitter_card-default.jpg", + type: spotifyPlaylist.type, + source: "spotify", + author: + spotifyPlaylist.type !== "playlist" + ? { + name: spotifyPlaylist.artists[0]?.name ?? "Unknown Artist", + url: spotifyPlaylist.artists[0]?.external_urls?.spotify ?? null + } + : { + name: spotifyPlaylist.owner?.display_name ?? spotifyPlaylist.owner?.id ?? "Unknown Artist", + url: spotifyPlaylist.owner?.external_urls?.spotify ?? null + }, + tracks: [], + id: spotifyPlaylist.id, + url: spotifyPlaylist.external_urls?.spotify ?? query + }); + + if (spotifyPlaylist.type !== "playlist") { + playlist.tracks = spotifyPlaylist.tracks.items.map((m: any) => { + const data = new Track(this, { + title: m.name ?? "", + description: m.description ?? "", + author: m.artists[0]?.name ?? "Unknown Artist", + url: m.external_urls?.spotify ?? query, + thumbnail: spotifyPlaylist.images[0]?.url ?? "https://www.scdn.co/i/_global/twitter_card-default.jpg", + duration: Util.buildTimeCode(Util.parseMS(m.duration_ms)), + views: 0, + requestedBy: options.requestedBy, + playlist, + source: "spotify" + }); + + return data; + }) as Track[]; + } else { + playlist.tracks = spotifyPlaylist.tracks.items.map((m: any) => { + const data = new Track(this, { + title: m.track.name ?? "", + description: m.track.description ?? "", + author: m.track.artists[0]?.name ?? "Unknown Artist", + url: m.track.external_urls?.spotify ?? query, + thumbnail: m.track.album?.images[0]?.url ?? "https://www.scdn.co/i/_global/twitter_card-default.jpg", + duration: Util.buildTimeCode(Util.parseMS(m.track.duration_ms)), + views: 0, + requestedBy: options.requestedBy, + playlist, + source: "spotify" + }); + + return data; + }) as Track[]; + } + + return { playlist: true, tracks: playlist.tracks }; } default: - return []; + return { playlist: false, tracks: [] }; } } diff --git a/src/Structures/Playlist.ts b/src/Structures/Playlist.ts index 04a81c2..676f0ca 100644 --- a/src/Structures/Playlist.ts +++ b/src/Structures/Playlist.ts @@ -1,18 +1,56 @@ import { Player } from "../Player"; import { Track } from "./Track"; +import { PlaylistInitData, PlaylistJSON, TrackJSON, TrackSource } from "../types/types"; class Playlist { public readonly player: Player; public tracks: Track[]; + public title: string; + public description: string; + public thumbnail: string; + public type: "album" | "playlist"; + public source: TrackSource; + public author: { + name: string; + url: string; + }; + public id: string; + public url: string; - constructor(player: Player, tracks: Track[]) { + constructor(player: Player, data: PlaylistInitData) { this.player = player; - this.tracks = tracks ?? []; + this.tracks = data.tracks ?? []; + this.author = data.author; + this.description = data.description; + this.thumbnail = data.thumbnail; + this.type = data.type; + this.source = data.source; + this.id = data.id; + this.url = data.url; + this.title = data.title; } *[Symbol.iterator]() { yield* this.tracks; } + + toJSON(withTracks = true) { + const payload = { + id: this.id, + url: this.url, + title: this.title, + description: this.description, + thumbnail: this.thumbnail, + type: this.type, + source: this.source, + author: this.author, + tracks: [] as TrackJSON[] + }; + + if (withTracks) payload.tracks = this.tracks.map((m) => m.toJSON()); + + return payload as PlaylistJSON; + } } export { Playlist }; diff --git a/src/Structures/Track.ts b/src/Structures/Track.ts index f4aca3a..4ac2c0b 100644 --- a/src/Structures/Track.ts +++ b/src/Structures/Track.ts @@ -1,6 +1,7 @@ import { User, Util } from "discord.js"; import { Player } from "../Player"; -import { RawTrackData } from "../types/types"; +import { RawTrackData, TrackJSON } from "../types/types"; +import { Playlist } from "./Playlist"; import { Queue } from "./Queue"; class Track { @@ -13,7 +14,7 @@ class Track { public duration!: string; public views!: number; public requestedBy!: User; - public fromPlaylist!: boolean; + public playlist?: Playlist; public readonly raw!: RawTrackData; public readonly _trackID = Date.now(); @@ -102,7 +103,7 @@ class Track { this.duration = data.duration ?? ""; this.views = data.views ?? 0; this.requestedBy = data.requestedBy; - this.fromPlaylist = Boolean(data.fromPlaylist); + this.playlist = data.playlist; // raw Object.defineProperty(this, "raw", { get: () => data.raw ?? data, enumerable: false }); @@ -164,9 +165,9 @@ class Track { duration: this.duration, durationMS: this.durationMS, views: this.views, - requested: this.requestedBy.id, - fromPlaylist: this.fromPlaylist - }; + requestedBy: this.requestedBy.id, + playlist: this.playlist?.toJSON(false) ?? null + } as TrackJSON; } } diff --git a/src/types/types.ts b/src/types/types.ts index f97994e..76faee8 100644 --- a/src/types/types.ts +++ b/src/types/types.ts @@ -1,8 +1,9 @@ -import { User } from "discord.js"; +import { Snowflake, User } from "discord.js"; import { downloadOptions } from "ytdl-core"; import { Readable, Duplex } from "stream"; import { Queue } from "../Structures/Queue"; import Track from "../Structures/Track"; +import { Playlist } from "../Structures/Playlist"; export type FiltersName = keyof QueueFilters; @@ -48,7 +49,7 @@ export interface RawTrackData { duration: string; views: number; requestedBy: User; - fromPlaylist: boolean; + playlist?: Playlist; source?: TrackSource; engine?: any; live?: boolean; @@ -162,3 +163,46 @@ export enum QueueRepeatMode { TRACK = 1, QUEUE = 2 } + +export interface PlaylistInitData { + tracks: Track[]; + title: string; + description: string; + thumbnail: string; + type: "album" | "playlist"; + source: TrackSource; + author: { + name: string; + url: string; + }; + id: string; + url: string; +} + +export interface TrackJSON { + title: string; + description: string; + author: string; + url: string; + thumbnail: string; + duration: string; + durationMS: number; + views: number; + requestedBy: Snowflake; + playlist?: PlaylistJSON; +} + +export interface PlaylistJSON { + id: string; + url: string; + title: string; + description: string; + thumbnail: string; + type: "album" | "playlist"; + source: TrackSource; + author: { + name: string; + url: string; + }; + tracks: TrackJSON[]; +}