UNPKG

ziplayer

Version:

A modular Discord voice player with plugin system

1,280 lines 49.7 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.Player = void 0; const events_1 = require("events"); const voice_1 = require("@discordjs/voice"); const Queue_1 = require("./Queue"); const plugins_1 = require("../plugins"); const timeout_1 = require("../utils/timeout"); /** * Represents a music player for a specific Discord guild. * * @example * // Create and configure player * const player = await manager.create(guildId, { * tts: { interrupt: true, volume: 1 }, * leaveOnEnd: true, * leaveTimeout: 30000 * }); * * // Connect to voice channel * await player.connect(voiceChannel); * * // Play different types of content * await player.play("Never Gonna Give You Up", userId); // Search query * await player.play("https://youtube.com/watch?v=dQw4w9WgXcQ", userId); // Direct URL * await player.play("tts: Hello everyone!", userId); // Text-to-Speech * * // Player controls * player.pause(); // Pause current track * player.resume(); // Resume paused track * player.skip(); // Skip to next track * player.stop(); // Stop and clear queue * player.setVolume(0.5); // Set volume to 50% * * // Event handling * player.on("trackStart", (player, track) => { * console.log(`Now playing: ${track.title}`); * }); * * player.on("queueEnd", (player) => { * console.log("Queue finished"); * }); * */ class Player extends events_1.EventEmitter { /** * Attach an extension to the player * * @param {BaseExtension} extension - The extension to attach * @example * player.attachExtension(new MyExtension()); */ attachExtension(extension) { if (this.extensions.includes(extension)) return; if (!extension.player) extension.player = this; this.extensions.push(extension); this.invokeExtensionLifecycle(extension, "onRegister"); } /** * Detach an extension from the player * * @param {BaseExtension} extension - The extension to detach * @example * player.detachExtension(new MyExtension()); */ detachExtension(extension) { const index = this.extensions.indexOf(extension); if (index === -1) return; this.extensions.splice(index, 1); this.invokeExtensionLifecycle(extension, "onDestroy"); if (extension.player === this) { extension.player = null; } } /** * Get all extensions attached to the player * * @returns {readonly BaseExtension[]} All attached extensions * @example * const extensions = player.getExtensions(); * console.log(`Extensions: ${extensions.length}`); */ getExtensions() { return this.extensions; } invokeExtensionLifecycle(extension, hook) { const fn = extension[hook]; if (typeof fn !== "function") return; try { const result = fn.call(extension, this.extensionContext); if (result && typeof result.then === "function") { result.catch((err) => this.debug(`[Player] Extension ${extension.name} ${hook} error:`, err)); } } catch (err) { this.debug(`[Player] Extension ${extension.name} ${hook} error:`, err); } } async runBeforePlayHooks(initial) { const request = { ...initial }; const response = {}; for (const extension of this.extensions) { const hook = extension.beforePlay; if (typeof hook !== "function") continue; try { const result = await Promise.resolve(hook.call(extension, this.extensionContext, request)); if (!result) continue; if (result.query !== undefined) { request.query = result.query; response.query = result.query; } if (result.requestedBy !== undefined) { request.requestedBy = result.requestedBy; response.requestedBy = result.requestedBy; } if (Array.isArray(result.tracks)) { response.tracks = result.tracks; } if (typeof result.isPlaylist === "boolean") { response.isPlaylist = result.isPlaylist; } if (typeof result.success === "boolean") { response.success = result.success; } if (result.error instanceof Error) { response.error = result.error; } if (typeof result.handled === "boolean") { response.handled = result.handled; if (result.handled) break; } } catch (err) { this.debug(`[Player] Extension ${extension.name} beforePlay error:`, err); } } return { request, response }; } async runAfterPlayHooks(payload) { if (this.extensions.length === 0) return; const safeTracks = payload.tracks ? [...payload.tracks] : undefined; if (safeTracks) { Object.freeze(safeTracks); } const immutablePayload = Object.freeze({ ...payload, tracks: safeTracks }); for (const extension of this.extensions) { const hook = extension.afterPlay; if (typeof hook !== "function") continue; try { await Promise.resolve(hook.call(extension, this.extensionContext, immutablePayload)); } catch (err) { this.debug(`[Player] Extension ${extension.name} afterPlay error:`, err); } } } async extensionsProvideSearch(query, requestedBy) { const request = { query, requestedBy }; for (const extension of this.extensions) { const hook = extension.provideSearch; if (typeof hook !== "function") continue; try { const result = await Promise.resolve(hook.call(extension, this.extensionContext, request)); if (result && Array.isArray(result.tracks) && result.tracks.length > 0) { this.debug(`[Player] Extension ${extension.name} handled search for query: ${query}`); return result; } } catch (err) { this.debug(`[Player] Extension ${extension.name} provideSearch error:`, err); } } return null; } async extensionsProvideStream(track) { const request = { track }; for (const extension of this.extensions) { const hook = extension.provideStream; if (typeof hook !== "function") continue; try { const result = await Promise.resolve(hook.call(extension, this.extensionContext, request)); if (result && result.stream) { this.debug(`[Player] Extension ${extension.name} provided stream for track: ${track.title}`); return result; } } catch (err) { this.debug(`[Player] Extension ${extension.name} provideStream error:`, err); } } return null; } /** * Start playing a specific track immediately, replacing the current resource. */ async startTrack(track) { try { let streamInfo = await this.extensionsProvideStream(track); let plugin; if (!streamInfo) { plugin = this.pluginManager.findPlugin(track.url) || this.pluginManager.get(track.source); if (!plugin) { this.debug(`[Player] No plugin found for track: ${track.title}`); throw new Error(`No plugin found for track: ${track.title}`); } this.debug(`[Player] Getting stream for track: ${track.title}`); this.debug(`[Player] Using plugin: ${plugin.name}`); this.debug(`[Track] Track Info:`, track); try { streamInfo = await (0, timeout_1.withTimeout)(plugin.getStream(track), this.options.extractorTimeout ?? 15000, "getStream timed out"); } catch (streamError) { this.debug(`[Player] getStream failed, trying getFallback:`, streamError); const allplugs = this.pluginManager.getAll(); for (const p of allplugs) { if (typeof p.getFallback !== "function") { continue; } try { streamInfo = await (0, timeout_1.withTimeout)(p.getFallback(track), this.options.extractorTimeout ?? 15000, `getFallback timed out for plugin ${p.name}`); if (!streamInfo?.stream) continue; this.debug(`[Player] getFallback succeeded with plugin ${p.name} for track: ${track.title}`); break; } catch (fallbackError) { this.debug(`[Player] getFallback failed with plugin ${p.name}:`, fallbackError); } } if (!streamInfo?.stream) { throw new Error(`All getFallback attempts failed for track: ${track.title}`); } } } else { this.debug(`[Player] Using extension-provided stream for track: ${track.title}`); } if (plugin) { this.debug(streamInfo); } // Kiểm tra nếu có stream thực sự để tạo AudioResource if (streamInfo && streamInfo.stream) { function mapToStreamType(type) { switch (type) { case "webm/opus": return voice_1.StreamType.WebmOpus; case "ogg/opus": return voice_1.StreamType.OggOpus; case "arbitrary": default: return voice_1.StreamType.Arbitrary; } } const stream = streamInfo.stream; const inputType = mapToStreamType(streamInfo.type); this.currentResource = (0, voice_1.createAudioResource)(stream, { metadata: track, inputType, inlineVolume: true, }); // Apply initial volume using the resource's VolumeTransformer if (this.volumeInterval) { clearInterval(this.volumeInterval); this.volumeInterval = null; } this.currentResource.volume?.setVolume(this.volume / 100); this.debug(`[Player] Playing resource for track: ${track.title}`); this.audioPlayer.play(this.currentResource); await (0, voice_1.entersState)(this.audioPlayer, voice_1.AudioPlayerStatus.Playing, 5000); return true; } else if (streamInfo && !streamInfo.stream) { // Extension đang xử lý phát nhạc (như Lavalink) - chỉ đánh dấu đang phát this.debug(`[Player] Extension is handling playback for track: ${track.title}`); this.isPlaying = true; this.isPaused = false; this.emit("trackStart", track); return true; } else { throw new Error(`No stream available for track: ${track.title}`); } } catch (error) { this.debug(`[Player] startTrack error:`, error); this.emit("playerError", error, track); return false; } } clearLeaveTimeout() { if (this.leaveTimeout) { clearTimeout(this.leaveTimeout); this.leaveTimeout = null; this.debug(`[Player] Cleared leave timeout`); } } debug(message, ...optionalParams) { if (this.listenerCount("debug") > 0) { this.emit("debug", message, ...optionalParams); } } constructor(guildId, options = {}, manager) { super(); this.connection = null; this.volume = 100; this.isPlaying = false; this.isPaused = false; this.leaveTimeout = null; this.currentResource = null; this.volumeInterval = null; this.skipLoop = false; this.extensions = []; // TTS support this.ttsPlayer = null; this.ttsQueue = []; this.ttsActive = false; this.debug(`[Player] Constructor called for guildId: ${guildId}`); this.guildId = guildId; this.queue = new Queue_1.Queue(); this.manager = manager; this.audioPlayer = (0, voice_1.createAudioPlayer)({ behaviors: { noSubscriber: voice_1.NoSubscriberBehavior.Pause, maxMissedFrames: 100, }, }); this.pluginManager = new plugins_1.PluginManager(); this.options = { leaveOnEnd: true, leaveOnEmpty: true, leaveTimeout: 100000, volume: 100, quality: "high", extractorTimeout: 50000, selfDeaf: true, selfMute: false, ...options, tts: { createPlayer: false, interrupt: true, volume: 100, Max_Time_TTS: 60000, ...(options?.tts || {}), }, }; this.volume = this.options.volume || 100; this.userdata = this.options.userdata; this.setupEventListeners(); this.extensionContext = Object.freeze({ player: this, manager }); // Optionally pre-create the TTS AudioPlayer if (this.options?.tts?.createPlayer) { this.ensureTTSPlayer(); } } setupEventListeners() { this.audioPlayer.on("stateChange", (oldState, newState) => { this.debug(`[Player] AudioPlayer stateChange from ${oldState.status} to ${newState.status}`); if (newState.status === voice_1.AudioPlayerStatus.Idle && oldState.status !== voice_1.AudioPlayerStatus.Idle) { // Track ended const track = this.queue.currentTrack; if (track) { this.debug(`[Player] Track ended: ${track.title}`); this.emit("trackEnd", track); } this.playNext(); } else if (newState.status === voice_1.AudioPlayerStatus.Playing && (oldState.status === voice_1.AudioPlayerStatus.Idle || oldState.status === voice_1.AudioPlayerStatus.Buffering)) { // Track started this.clearLeaveTimeout(); this.isPlaying = true; this.isPaused = false; const track = this.queue.currentTrack; if (track) { this.debug(`[Player] Track started: ${track.title}`); this.emit("trackStart", track); } } else if (newState.status === voice_1.AudioPlayerStatus.Paused && oldState.status !== voice_1.AudioPlayerStatus.Paused) { // Track paused this.isPaused = true; const track = this.queue.currentTrack; if (track) { this.debug(`[Player] Player paused on track: ${track.title}`); this.emit("playerPause", track); } } else if (newState.status !== voice_1.AudioPlayerStatus.Paused && oldState.status === voice_1.AudioPlayerStatus.Paused) { // Track resumed this.isPaused = false; const track = this.queue.currentTrack; if (track) { this.debug(`[Player] Player resumed on track: ${track.title}`); this.emit("playerResume", track); } } else if (newState.status === voice_1.AudioPlayerStatus.AutoPaused) { this.debug(`[Player] AudioPlayerStatus.AutoPaused`); } else if (newState.status === voice_1.AudioPlayerStatus.Buffering) { this.debug(`[Player] AudioPlayerStatus.Buffering`); } }); this.audioPlayer.on("error", (error) => { this.debug(`[Player] AudioPlayer error:`, error); this.emit("playerError", error, this.queue.currentTrack || undefined); this.playNext(); }); this.audioPlayer.on("debug", (...args) => { if (this.manager.debugEnabled) { this.emit("debug", ...args); } }); } ensureTTSPlayer() { if (this.ttsPlayer) return this.ttsPlayer; this.ttsPlayer = (0, voice_1.createAudioPlayer)({ behaviors: { noSubscriber: voice_1.NoSubscriberBehavior.Pause, maxMissedFrames: 100, }, }); this.ttsPlayer.on("error", (e) => this.debug("[TTS] error:", e)); return this.ttsPlayer; } addPlugin(plugin) { this.debug(`[Player] Adding plugin: ${plugin.name}`); this.pluginManager.register(plugin); } removePlugin(name) { this.debug(`[Player] Removing plugin: ${name}`); return this.pluginManager.unregister(name); } /** * Connect to a voice channel * * @param {VoiceChannel} channel - Discord voice channel * @returns {Promise<VoiceConnection>} The voice connection * @example * await player.connect(voiceChannel); */ async connect(channel) { try { this.debug(`[Player] Connecting to voice channel: ${channel.id}`); const connection = (0, voice_1.joinVoiceChannel)({ channelId: channel.id, guildId: channel.guildId, adapterCreator: channel.guild.voiceAdapterCreator, selfDeaf: this.options.selfDeaf ?? true, selfMute: this.options.selfMute ?? false, }); await (0, voice_1.entersState)(connection, voice_1.VoiceConnectionStatus.Ready, 50000); this.connection = connection; connection.on(voice_1.VoiceConnectionStatus.Disconnected, () => { this.debug(`[Player] VoiceConnectionStatus.Disconnected`); this.destroy(); }); connection.on("error", (error) => { this.debug(`[Player] Voice connection error:`, error); this.emit("connectionError", error); }); connection.subscribe(this.audioPlayer); this.clearLeaveTimeout(); return this.connection; } catch (error) { this.debug(`[Player] Connection error:`, error); this.emit("connectionError", error); this.connection?.destroy(); throw error; } } /** * Search for tracks using the player's extensions and plugins * * @param {string} query - The query to search for * @param {string} requestedBy - The user ID who requested the search * @returns {Promise<SearchResult>} The search result * @example * const result = await player.search("Never Gonna Give You Up", userId); * console.log(`Search result: ${result.tracks.length} tracks`); */ async search(query, requestedBy) { this.debug(`[Player] Search called with query: ${query}, requestedBy: ${requestedBy}`); const extensionResult = await this.extensionsProvideSearch(query, requestedBy); if (extensionResult && Array.isArray(extensionResult.tracks) && extensionResult.tracks.length > 0) { this.debug(`[Player] Extension handled search for query: ${query}`); return extensionResult; } const plugins = this.pluginManager.getAll(); let lastError = null; for (const p of plugins) { try { this.debug(`[Player] Trying plugin for search: ${p.name}`); const res = await (0, timeout_1.withTimeout)(p.search(query, requestedBy), this.options.extractorTimeout ?? 15000, `Search operation timed out for ${p.name}`); if (res && Array.isArray(res.tracks) && res.tracks.length > 0) { this.debug(`[Player] Plugin '${p.name}' returned ${res.tracks.length} tracks`); return res; } this.debug(`[Player] Plugin '${p.name}' returned no tracks`); } catch (error) { lastError = error; this.debug(`[Player] Search via plugin '${p.name}' failed:`, error); // Continue to next plugin } } this.debug(`[Player] No plugins returned results for query: ${query}`); if (lastError) this.emit("playerError", lastError); throw new Error(`No plugin found to handle: ${query}`); } /** * Play a track or search query * * @param {string | Track} query - Track URL, search query, or Track object * @param {string} requestedBy - User ID who requested the track * @returns {Promise<boolean>} True if playback started successfully * @example * await player.play("Never Gonna Give You Up", userId); * await player.play("https://youtube.com/watch?v=dQw4w9WgXcQ", userId); * await player.play("tts: Hello everyone!", userId); */ async play(query, requestedBy) { this.debug(`[Player] Play called with query: ${typeof query === "string" ? query : query?.title}`); this.clearLeaveTimeout(); let tracksToAdd = []; let isPlaylist = false; let effectiveRequest = { query, requestedBy }; let hookResponse = {}; try { const hookOutcome = await this.runBeforePlayHooks(effectiveRequest); effectiveRequest = hookOutcome.request; hookResponse = hookOutcome.response; if (effectiveRequest.requestedBy === undefined) { effectiveRequest.requestedBy = requestedBy; } const hookTracks = Array.isArray(hookResponse.tracks) ? hookResponse.tracks : undefined; if (hookResponse.handled && (!hookTracks || hookTracks.length === 0)) { const handledPayload = { success: hookResponse.success ?? true, query: effectiveRequest.query, requestedBy: effectiveRequest.requestedBy, tracks: [], isPlaylist: hookResponse.isPlaylist ?? false, error: hookResponse.error, }; await this.runAfterPlayHooks(handledPayload); if (hookResponse.error) { this.emit("playerError", hookResponse.error); } return hookResponse.success ?? true; } if (hookTracks && hookTracks.length > 0) { tracksToAdd = hookTracks; isPlaylist = hookResponse.isPlaylist ?? hookTracks.length > 1; } else if (typeof effectiveRequest.query === "string") { const searchResult = await this.search(effectiveRequest.query, effectiveRequest.requestedBy || "Unknown"); tracksToAdd = searchResult.tracks; if (searchResult.playlist) { isPlaylist = true; this.debug(`[Player] Added playlist: ${searchResult.playlist.name} (${tracksToAdd.length} tracks)`); } } else if (effectiveRequest.query) { tracksToAdd = [effectiveRequest.query]; } if (tracksToAdd.length === 0) { this.debug(`[Player] No tracks found for play`); throw new Error("No tracks found"); } const isTTS = (t) => { if (!t) return false; try { return typeof t.source === "string" && t.source.toLowerCase().includes("tts"); } catch { return false; } }; const queryLooksTTS = typeof effectiveRequest.query === "string" && effectiveRequest.query.trim().toLowerCase().startsWith("tts"); if (!isPlaylist && tracksToAdd.length > 0 && this.options?.tts?.interrupt !== false && (isTTS(tracksToAdd[0]) || queryLooksTTS)) { this.debug(`[Player] Interrupting with TTS: ${tracksToAdd[0].title}`); await this.interruptWithTTSTrack(tracksToAdd[0]); await this.runAfterPlayHooks({ success: true, query: effectiveRequest.query, requestedBy: effectiveRequest.requestedBy, tracks: tracksToAdd, isPlaylist, }); return true; } if (isPlaylist) { this.queue.addMultiple(tracksToAdd); this.emit("queueAddList", tracksToAdd); } else { this.queue.add(tracksToAdd[0]); this.emit("queueAdd", tracksToAdd[0]); } const started = !this.isPlaying ? await this.playNext() : true; await this.runAfterPlayHooks({ success: started, query: effectiveRequest.query, requestedBy: effectiveRequest.requestedBy, tracks: tracksToAdd, isPlaylist, }); return started; } catch (error) { await this.runAfterPlayHooks({ success: false, query: effectiveRequest.query, requestedBy: effectiveRequest.requestedBy, tracks: tracksToAdd, isPlaylist, error: error, }); this.debug(`[Player] Play error:`, error); this.emit("playerError", error); return false; } } /** * Interrupt current music with a TTS track. Pauses music, swaps the * subscription to a dedicated TTS player, plays TTS, then resumes. * * @param {Track} track - The track to interrupt with * @returns {Promise<void>} * @example * await player.interruptWithTTSTrack(track); */ async interruptWithTTSTrack(track) { this.ttsQueue.push(track); if (!this.ttsActive) { void this.playNextTTS(); } } /** * Play queued TTS items sequentially * * @returns {Promise<void>} * @example * await player.playNextTTS(); */ async playNextTTS() { const next = this.ttsQueue.shift(); if (!next) return; this.ttsActive = true; try { if (!this.connection) throw new Error("No voice connection for TTS"); const ttsPlayer = this.ensureTTSPlayer(); // Build resource from plugin stream const resource = await this.resourceFromTrack(next); if (resource.volume) { resource.volume.setVolume((this.options?.tts?.volume ?? this?.volume ?? 100) / 100); } const wasPlaying = this.audioPlayer.state.status === voice_1.AudioPlayerStatus.Playing || this.audioPlayer.state.status === voice_1.AudioPlayerStatus.Buffering; // Pause current music if any try { this.audioPlayer.pause(true); } catch { } // Swap subscription and play TTS this.connection.subscribe(ttsPlayer); this.emit("ttsStart", { track: next }); ttsPlayer.play(resource); // Wait until TTS starts then finishes await (0, voice_1.entersState)(ttsPlayer, voice_1.AudioPlayerStatus.Playing, 5000).catch(() => null); // Derive timeout from resource/track duration when available, with a sensible cap const md = resource?.metadata ?? {}; const declared = typeof md.duration === "number" ? md.duration : typeof next?.duration === "number" ? next.duration : undefined; const declaredMs = declared ? declared > 1000 ? declared : declared * 1000 : undefined; const cap = this.options?.tts?.Max_Time_TTS ?? 60000; const idleTimeout = declaredMs ? Math.min(cap, Math.max(1000, declaredMs + 1500)) : cap; await (0, voice_1.entersState)(ttsPlayer, voice_1.AudioPlayerStatus.Idle, idleTimeout).catch(() => null); // Swap back and resume if needed this.connection.subscribe(this.audioPlayer); if (wasPlaying) { try { this.audioPlayer.unpause(); } catch { } } this.emit("ttsEnd"); } catch (err) { this.debug("[TTS] error while playing:", err); this.emit("playerError", err); } finally { this.ttsActive = false; if (this.ttsQueue.length > 0) { await this.playNextTTS(); } } } /** Build AudioResource for a given track using the plugin pipeline */ async resourceFromTrack(track) { // Resolve plugin similar to playNext const plugin = this.pluginManager.findPlugin(track.url) || this.pluginManager.get(track.source); if (!plugin) throw new Error(`No plugin found for track: ${track.title}`); let streamInfo; try { streamInfo = await (0, timeout_1.withTimeout)(plugin.getStream(track), this.options.extractorTimeout ?? 15000, "getStream timed out"); } catch (streamError) { // try fallbacks const allplugs = this.pluginManager.getAll(); for (const p of allplugs) { if (typeof p.getFallback !== "function") continue; try { streamInfo = await (0, timeout_1.withTimeout)(p.getFallback(track), this.options.extractorTimeout ?? 15000, `getFallback timed out for plugin ${p.name}`); if (!streamInfo?.stream) continue; break; } catch { } } if (!streamInfo?.stream) throw new Error(`All getFallback attempts failed for track: ${track.title}`); } const mapToStreamType = (type) => { switch (type) { case "webm/opus": return voice_1.StreamType.WebmOpus; case "ogg/opus": return voice_1.StreamType.OggOpus; case "arbitrary": default: return voice_1.StreamType.Arbitrary; } }; const inputType = mapToStreamType(streamInfo.type); return (0, voice_1.createAudioResource)(streamInfo.stream, { // Prefer plugin-provided metadata (e.g., precise duration), fallback to track fields metadata: { ...track, ...(streamInfo?.metadata || {}), }, inputType, inlineVolume: true, }); } async generateWillNext() { const lastTrack = this.queue.previousTracks[this.queue.previousTracks.length - 1] ?? this.queue.currentTrack; if (!lastTrack) return; // Build list of candidate plugins: preferred first, then others with getRelatedTracks const preferred = this.pluginManager.findPlugin(lastTrack.url) || this.pluginManager.get(lastTrack.source); const all = this.pluginManager.getAll(); const candidates = [...(preferred ? [preferred] : []), ...all.filter((p) => p !== preferred)].filter((p) => typeof p.getRelatedTracks === "function"); for (const p of candidates) { try { this.debug(`[Player] Trying related from plugin: ${p.name}`); const related = await (0, timeout_1.withTimeout)(p.getRelatedTracks(lastTrack.url, { limit: 10, history: this.queue.previousTracks, }), this.options.extractorTimeout ?? 15000, `getRelatedTracks timed out for ${p.name}`); if (Array.isArray(related) && related.length > 0) { const randomchoice = Math.floor(Math.random() * related.length); const nextTrack = this.queue.nextTrack ? this.queue.nextTrack : related[randomchoice]; this.queue.willNextTrack(nextTrack); this.queue.relatedTracks(related); this.debug(`[Player] Will next track if autoplay: ${nextTrack?.title} (via ${p.name})`); this.emit("willPlay", nextTrack, related); return; // success } this.debug(`[Player] ${p.name} returned no related tracks`); } catch (err) { this.debug(`[Player] getRelatedTracks error from ${p.name}:`, err); // try next candidate } } } async playNext() { this.debug(`[Player] playNext called`); const track = this.queue.next(this.skipLoop); this.skipLoop = false; if (!track) { if (this.queue.autoPlay()) { const willnext = this.queue.willNextTrack(); if (willnext) { this.debug(`[Player] Auto-playing next track: ${willnext.title}`); this.queue.addMultiple([willnext]); return this.playNext(); } } this.debug(`[Player] No next track in queue`); this.isPlaying = false; this.emit("queueEnd"); if (this.options.leaveOnEnd) { this.scheduleLeave(); } return false; } this.generateWillNext(); // A new track is about to play; ensure we don't leave mid-playback this.clearLeaveTimeout(); try { return await this.startTrack(track); } catch (error) { this.debug(`[Player] playNext error:`, error); this.emit("playerError", error, track); return this.playNext(); } } /** * Pause the current track * * @returns {boolean} True if paused successfully * @example * const paused = player.pause(); * console.log(`Paused: ${paused}`); */ pause() { this.debug(`[Player] pause called`); if (this.isPlaying && !this.isPaused) { return this.audioPlayer.pause(); } return false; } /** * Resume the current track * * @returns {boolean} True if resumed successfully * @example * const resumed = player.resume(); * console.log(`Resumed: ${resumed}`); */ resume() { this.debug(`[Player] resume called`); if (this.isPaused) { const result = this.audioPlayer.unpause(); if (result) { const track = this.queue.currentTrack; if (track) { this.debug(`[Player] Player resumed on track: ${track.title}`); this.emit("playerResume", track); } } return result; } return false; } /** * Stop the current track * * @returns {boolean} True if stopped successfully * @example * const stopped = player.stop(); * console.log(`Stopped: ${stopped}`); */ stop() { this.debug(`[Player] stop called`); this.queue.clear(); const result = this.audioPlayer.stop(); this.isPlaying = false; this.isPaused = false; this.emit("playerStop"); return result; } /** * Skip to the next track * * @returns {boolean} True if skipped successfully * @example * const skipped = player.skip(); * console.log(`Skipped: ${skipped}`); */ skip() { this.debug(`[Player] skip called`); if (this.isPlaying || this.isPaused) { this.skipLoop = true; return this.audioPlayer.stop(); } return !!this.playNext(); } /** * Go back to the previous track in history and play it. * * @returns {Promise<boolean>} True if previous track was played successfully * @example * const previous = await player.previous(); * console.log(`Previous: ${previous}`); */ async previous() { this.debug(`[Player] previous called`); const track = this.queue.previous(); if (!track) return false; if (this.queue.currentTrack) this.insert(this.queue.currentTrack, 0); this.clearLeaveTimeout(); return this.startTrack(track); } /** * Loop the current track * * @param {LoopMode} mode - The loop mode to set * @returns {LoopMode} The loop mode * @example * const loopMode = player.loop("track"); * console.log(`Loop mode: ${loopMode}`); */ loop(mode) { return this.queue.loop(mode); } /** * Set the auto-play mode * * @param {boolean} mode - The auto-play mode to set * @returns {boolean} The auto-play mode * @example * const autoPlayMode = player.autoPlay(true); * console.log(`Auto-play mode: ${autoPlayMode}`); */ autoPlay(mode) { return this.queue.autoPlay(mode); } /** * Set the volume of the current track * * @param {number} volume - The volume to set * @returns {boolean} True if volume was set successfully * @example * const volumeSet = player.setVolume(50); * console.log(`Volume set: ${volumeSet}`); */ setVolume(volume) { this.debug(`[Player] setVolume called: ${volume}`); if (volume < 0 || volume > 200) return false; const oldVolume = this.volume; this.volume = volume; const resourceVolume = this.currentResource?.volume; if (resourceVolume) { if (this.volumeInterval) clearInterval(this.volumeInterval); const start = resourceVolume.volume; const target = this.volume / 100; const steps = 10; let currentStep = 0; this.volumeInterval = setInterval(() => { currentStep++; const value = start + ((target - start) * currentStep) / steps; resourceVolume.setVolume(value); if (currentStep >= steps) { clearInterval(this.volumeInterval); this.volumeInterval = null; } }, 300); } this.emit("volumeChange", oldVolume, volume); return true; } /** * Shuffle the queue * * @returns {void} * @example * player.shuffle(); */ shuffle() { this.debug(`[Player] shuffle called`); this.queue.shuffle(); } /** * Clear the queue * * @returns {void} * @example * player.clearQueue(); */ clearQueue() { this.debug(`[Player] clearQueue called`); this.queue.clear(); } /** * Insert a track or list of tracks into the upcoming queue at a specific position (0 = play after current). * - If `query` is a string, performs a search and inserts resulting tracks (playlist supported). * - If a Track or Track[] is provided, inserts directly. * Does not auto-start playback; it only modifies the queue. * * @param {string | Track | Track[]} query - The track or tracks to insert * @param {number} index - The index to insert the tracks at * @param {string} requestedBy - The user ID who requested the insert * @returns {Promise<boolean>} True if the tracks were inserted successfully * @example * const inserted = await player.insert("Song Name", 0, userId); * console.log(`Inserted: ${inserted}`); */ async insert(query, index, requestedBy) { try { this.debug(`[Player] insert called at index ${index} with type: ${typeof query}`); let tracksToAdd = []; let isPlaylist = false; if (typeof query === "string") { const searchResult = await this.search(query, requestedBy || "Unknown"); tracksToAdd = searchResult.tracks || []; isPlaylist = !!searchResult.playlist; } else if (Array.isArray(query)) { tracksToAdd = query; isPlaylist = query.length > 1; } else if (query) { tracksToAdd = [query]; } if (!tracksToAdd || tracksToAdd.length === 0) { this.debug(`[Player] insert: no tracks resolved`); throw new Error("No tracks to insert"); } if (tracksToAdd.length === 1) { this.queue.insert(tracksToAdd[0], index); this.emit("queueAdd", tracksToAdd[0]); this.debug(`[Player] Inserted track at index ${index}: ${tracksToAdd[0].title}`); } else { this.queue.insertMultiple(tracksToAdd, index); this.emit("queueAddList", tracksToAdd); this.debug(`[Player] Inserted ${tracksToAdd.length} ${isPlaylist ? "playlist " : ""}tracks at index ${index}`); } return true; } catch (error) { this.debug(`[Player] insert error:`, error); this.emit("playerError", error); return false; } } /** * Remove a track from the queue * * @param {number} index - The index of the track to remove * @returns {Track | null} The removed track or null * @example * const removed = player.remove(0); * console.log(`Removed: ${removed?.title}`); */ remove(index) { this.debug(`[Player] remove called for index: ${index}`); const track = this.queue.remove(index); if (track) { this.emit("queueRemove", track, index); } return track; } /** * Get the progress bar of the current track * * @param {ProgressBarOptions} options - The options for the progress bar * @returns {string} The progress bar * @example * const progressBar = player.getProgressBar(); * console.log(`Progress bar: ${progressBar}`); */ getProgressBar(options = {}) { const { size = 20, barChar = "▬", progressChar = "🔘" } = options; const track = this.queue.currentTrack; const resource = this.currentResource; if (!track || !resource) return ""; const total = track.duration > 1000 ? track.duration : track.duration * 1000; if (!total) return this.formatTime(resource.playbackDuration); const current = resource.playbackDuration; const ratio = Math.min(current / total, 1); const progress = Math.round(ratio * size); const bar = barChar.repeat(progress) + progressChar + barChar.repeat(size - progress); return `${this.formatTime(current)} | ${bar} | ${this.formatTime(total)}`; } /** * Get the time of the current track * * @returns {Object} The time of the current track * @example * const time = player.getTime(); * console.log(`Time: ${time.current}`); */ getTime() { const resource = this.currentResource; const track = this.queue.currentTrack; if (!track || !resource) return { current: 0, total: 0, format: "00:00", }; const total = track.duration > 1000 ? track.duration : track.duration * 1000; return { current: resource?.playbackDuration, total: total, format: this.formatTime(resource.playbackDuration), }; } /** * Format the time in the format of HH:MM:SS * * @param {number} ms - The time in milliseconds * @returns {string} The formatted time * @example * const formattedTime = player.formatTime(1000); * console.log(`Formatted time: ${formattedTime}`); */ formatTime(ms) { const totalSeconds = Math.floor(ms / 1000); const hours = Math.floor(totalSeconds / 3600); const minutes = Math.floor((totalSeconds % 3600) / 60); const seconds = totalSeconds % 60; const parts = []; if (hours > 0) parts.push(String(hours).padStart(2, "0")); parts.push(String(minutes).padStart(2, "0")); parts.push(String(seconds).padStart(2, "0")); return parts.join(":"); } scheduleLeave() { this.debug(`[Player] scheduleLeave called`); if (this.leaveTimeout) { clearTimeout(this.leaveTimeout); } if (this.options.leaveOnEmpty && this.options.leaveTimeout) { this.leaveTimeout = setTimeout(() => { this.debug(`[Player] Leaving voice channel after timeout`); this.destroy(); }, this.options.leaveTimeout); } } /** * Destroy the player * * @returns {void} * @example * player.destroy(); */ destroy() { this.debug(`[Player] destroy called`); if (this.leaveTimeout) { clearTimeout(this.leaveTimeout); this.leaveTimeout = null; } this.audioPlayer.stop(true); if (this.ttsPlayer) { try { this.ttsPlayer.stop(true); } catch { } this.ttsPlayer = null; } if (this.connection) { this.connection.destroy(); this.connection = null; } this.queue.clear(); this.pluginManager.clear(); for (const extension of [...this.extensions]) { this.invokeExtensionLifecycle(extension, "onDestroy"); if (extension.player === this) { extension.player = null; } } this.extensions = []; this.isPlaying = false; this.isPaused = false; this.emit("playerDestroy"); this.removeAllListeners(); } /** * Get the size of the queue * * @returns {number} The size of the queue * @example * const queueSize = player.queueSize; * console.log(`Queue size: ${queueSize}`); */ get queueSize() { return this.queue.size; } /** * Get the current track * * @returns {Track | null} The current track or null * @example * const currentTrack = player.currentTrack; * console.log(`Current track: ${currentTrack?.title}`); */ get currentTrack() { return this.queue.currentTrack; } /** * Get the previous track * * @returns {Track | null} The previous track or null * @example * const previousTrack = player.previousTrack; * console.log(`Previous track: ${previousTrack?.title}`); */ get previousTrack() { return this.queue.previousTracks?.at(-1) ?? null; } /** * Get the upcoming tracks * * @returns {Track[]} The upcoming tracks * @example * const upcomingTracks = player.upcomingTracks; * console.log(`Upcoming tracks: ${upcomingTracks.length}`); */ get upcomingTracks() { return this.queue.getTracks(); } /** * Get the previous tracks * * @returns {Track[]} The previous tracks * @example * const previousTracks = player.previousTracks; * console.log(`Previous tracks: ${previousTracks.length}`); */ get previousTracks() { return this.queue.previousTracks; } /** * Get the available plugins * * @returns {string[]} The available plugins * @example * const availablePlugins = player.availablePlugins; * console.log(`Available plugins: ${availablePlugins.length}`); */ get availablePlugins() { return this.pluginManager.getAll().map((p) => p.name); } /** * Get the related tracks * * @returns {Track[] | null} The related tracks or null * @example * const relatedTracks = player.relatedTracks; * console.log(`Related tracks: ${relatedTracks?.length}`); */ get relatedTracks() { return this.queue.relatedTracks(); } } exports.Player = Player; //# sourceMappingURL=Player.js.map