var __defProp = Object.defineProperty;
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });

// src/constant.ts
var version = "5.2.3";
var AUDIO_SAMPLE_RATE = 48e3;
var AUDIO_CHANNELS = 2;
var DEFAULT_VOLUME = 50;
var JOIN_TIMEOUT_MS = 3e4;
var RECONNECT_TIMEOUT_MS = 5e3;
var RECONNECT_MAX_ATTEMPTS = 5;
var HTTP_REDIRECT_CODES = /* @__PURE__ */ new Set([301, 302, 303, 307, 308]);
var MAX_REDIRECT_DEPTH = 5;
var defaultFilters = {
  "3d": "apulsator=hz=0.125",
  bassboost: "bass=g=10",
  echo: "aecho=0.8:0.9:1000:0.3",
  flanger: "flanger",
  gate: "agate",
  haas: "haas",
  karaoke: "stereotools=mlev=0.1",
  nightcore: "asetrate=48000*1.25,aresample=48000,bass=g=5",
  reverse: "areverse",
  vaporwave: "asetrate=48000*0.8,aresample=48000,atempo=1.1",
  mcompand: "mcompand",
  phaser: "aphaser",
  tremolo: "tremolo",
  surround: "surround",
  earwax: "earwax"
};
var defaultOptions = {
  plugins: [],
  emitNewSongOnly: false,
  savePreviousSongs: true,
  nsfw: false,
  emitAddSongWhenCreatingQueue: true,
  emitAddListWhenCreatingQueue: true,
  joinNewVoiceChannel: true
};

// src/core/DisTubeBase.ts
var DisTubeBase = class {
  static {
    __name(this, "DisTubeBase");
  }
  distube;
  constructor(distube) {
    this.distube = distube;
  }
  /**
   * Emit the {@link DisTube} of this base
   * @param eventName - Event name
   * @param args      - arguments
   */
  emit(eventName, ...args) {
    return this.distube.emit(eventName, ...args);
  }
  /**
   * Emit error event
   * @param error   - error
   * @param queue   - The queue encountered the error
   * @param song    - The playing song when encountered the error
   */
  emitError(error, queue, song) {
    this.distube.emitError(error, queue, song);
  }
  /**
   * Emit debug event
   * @param message - debug message
   */
  debug(message) {
    this.distube.debug(message);
  }
  /**
   * The queue manager
   */
  get queues() {
    return this.distube.queues;
  }
  /**
   * The voice manager
   */
  get voices() {
    return this.distube.voices;
  }
  /**
   * Discord.js client
   */
  get client() {
    return this.distube.client;
  }
  /**
   * DisTube options
   */
  get options() {
    return this.distube.options;
  }
  /**
   * DisTube handler
   */
  get handler() {
    return this.distube.handler;
  }
  /**
   * DisTube plugins
   */
  get plugins() {
    return this.distube.plugins;
  }
};

// src/core/DisTubeHandler.ts
import { request } from "undici";

// src/struct/DisTubeError.ts
import { inspect } from "util";
var ERROR_MESSAGES = {
  INVALID_TYPE: /* @__PURE__ */ __name((expected, got, name) => `Expected ${Array.isArray(expected) ? expected.map((e) => typeof e === "number" ? e : `'${e}'`).join(" or ") : `'${expected}'`}${name ? ` for '${name}'` : ""}, but got ${inspect(got)} (${typeof got})`, "INVALID_TYPE"),
  NUMBER_COMPARE: /* @__PURE__ */ __name((name, expected, value) => `'${name}' must be ${expected} ${value}`, "NUMBER_COMPARE"),
  EMPTY_ARRAY: /* @__PURE__ */ __name((name) => `'${name}' is an empty array`, "EMPTY_ARRAY"),
  EMPTY_FILTERED_ARRAY: /* @__PURE__ */ __name((name, type) => `There is no valid '${type}' in the '${name}' array`, "EMPTY_FILTERED_ARRAY"),
  EMPTY_STRING: /* @__PURE__ */ __name((name) => `'${name}' string must not be empty`, "EMPTY_STRING"),
  INVALID_KEY: /* @__PURE__ */ __name((obj, key) => `'${key}' does not need to be provided in ${obj}`, "INVALID_KEY"),
  MISSING_KEY: /* @__PURE__ */ __name((obj, key) => `'${key}' needs to be provided in ${obj}`, "MISSING_KEY"),
  MISSING_KEYS: /* @__PURE__ */ __name((obj, key, all) => `${key.map((k) => `'${k}'`).join(all ? " and " : " or ")} need to be provided in ${obj}`, "MISSING_KEYS"),
  MISSING_INTENTS: /* @__PURE__ */ __name((i) => `${i} intent must be provided for the Client`, "MISSING_INTENTS"),
  DISABLED_OPTION: /* @__PURE__ */ __name((o) => `DisTubeOptions.${o} is disabled`, "DISABLED_OPTION"),
  ENABLED_OPTION: /* @__PURE__ */ __name((o) => `DisTubeOptions.${o} is enabled`, "ENABLED_OPTION"),
  NOT_IN_VOICE: "User is not in any voice channel",
  VOICE_FULL: "The voice channel is full",
  VOICE_ALREADY_CREATED: "This guild already has a voice connection which is not managed by DisTube",
  VOICE_CONNECT_FAILED: /* @__PURE__ */ __name((s) => `Cannot connect to the voice channel after ${s} seconds`, "VOICE_CONNECT_FAILED"),
  VOICE_MISSING_PERMS: "I do not have permission to join this voice channel",
  VOICE_RECONNECT_FAILED: "Cannot reconnect to the voice channel",
  VOICE_DIFFERENT_GUILD: "Cannot join a voice channel in a different guild",
  VOICE_DIFFERENT_CLIENT: "Cannot join a voice channel created by a different client",
  FFMPEG_EXITED: /* @__PURE__ */ __name((code) => `ffmpeg exited with code ${code}`, "FFMPEG_EXITED"),
  FFMPEG_NOT_INSTALLED: /* @__PURE__ */ __name((path) => `ffmpeg is not installed at '${path}' path`, "FFMPEG_NOT_INSTALLED"),
  ENCRYPTION_LIBRARIES_MISSING: "Cannot play audio as no valid encryption package is installed and your node doesn't support aes-256-gcm.\nPlease install  @noble/ciphers, @stablelib/xchacha20poly1305, sodium-native or libsodium-wrappers.",
  NO_QUEUE: "There is no playing queue in this guild",
  QUEUE_EXIST: "This guild has a Queue already",
  QUEUE_STOPPED: "The queue has been stopped already",
  PAUSED: "The queue has been paused already",
  RESUMED: "The queue has been playing already",
  NO_PREVIOUS: "There is no previous song in this queue",
  NO_UP_NEXT: "There is no up next song",
  NO_SONG_POSITION: "Does not have any song at this position",
  NO_PLAYING_SONG: "There is no playing song in the queue",
  NO_EXTRACTOR_PLUGIN: "There is no extractor plugin in the DisTubeOptions.plugins, please add one for searching songs",
  NO_RELATED: "Cannot find any related songs",
  CANNOT_PLAY_RELATED: "Cannot play the related song",
  UNAVAILABLE_VIDEO: "This video is unavailable",
  UNPLAYABLE_FORMATS: "No playable format found",
  NON_NSFW: "Cannot play age-restricted content in non-NSFW channel",
  NOT_SUPPORTED_URL: "This url is not supported",
  NOT_SUPPORTED_SONG: /* @__PURE__ */ __name((song) => `There is no plugin supporting this song (${song})`, "NOT_SUPPORTED_SONG"),
  NO_VALID_SONG: "'songs' array does not have any valid Song or url",
  CANNOT_RESOLVE_SONG: /* @__PURE__ */ __name((t) => `Cannot resolve ${inspect(t)} to a Song`, "CANNOT_RESOLVE_SONG"),
  CANNOT_GET_STREAM_URL: /* @__PURE__ */ __name((song) => `Cannot get stream url with this song (${song})`, "CANNOT_GET_STREAM_URL"),
  CANNOT_GET_SEARCH_QUERY: /* @__PURE__ */ __name((song) => `Cannot get search query with this song (${song})`, "CANNOT_GET_SEARCH_QUERY"),
  NO_RESULT: /* @__PURE__ */ __name((query) => `Cannot find any song with this query (${query})`, "NO_RESULT"),
  NO_STREAM_URL: /* @__PURE__ */ __name((song) => `No stream url attached (${song})`, "NO_STREAM_URL"),
  EMPTY_FILTERED_PLAYLIST: "There is no valid video in the playlist\nMaybe age-restricted contents is filtered because you are in non-NSFW channel",
  EMPTY_PLAYLIST: "There is no valid video in the playlist"
};
var haveCode = /* @__PURE__ */ __name((code) => Object.keys(ERROR_MESSAGES).includes(code), "haveCode");
var parseMessage = /* @__PURE__ */ __name((m, ...args) => typeof m === "string" ? m : m(...args), "parseMessage");
var getErrorMessage = /* @__PURE__ */ __name((code, ...args) => haveCode(code) ? parseMessage(ERROR_MESSAGES[code], ...args) : args[0], "getErrorMessage");
var DisTubeError = class _DisTubeError extends Error {
  static {
    __name(this, "DisTubeError");
  }
  errorCode;
  constructor(code, ...args) {
    super(getErrorMessage(code, ...args));
    this.errorCode = code;
    if (Error.captureStackTrace) Error.captureStackTrace(this, _DisTubeError);
  }
  get name() {
    return `DisTubeError [${this.errorCode}]`;
  }
  get code() {
    return this.errorCode;
  }
};

// src/util.ts
import { URL as URL2 } from "url";
import { Constants as Constants2, GatewayIntentBits, IntentsBitField, SnowflakeUtil } from "discord.js";

// src/core/DisTubeVoice.ts
import {
  AudioPlayerStatus,
  createAudioPlayer,
  entersState,
  joinVoiceChannel,
  VoiceConnectionDisconnectReason,
  VoiceConnectionStatus
} from "@discordjs/voice";
import { Constants } from "discord.js";
import { TypedEmitter } from "tiny-typed-emitter";
var DisTubeVoice = class extends TypedEmitter {
  static {
    __name(this, "DisTubeVoice");
  }
  id;
  voices;
  audioPlayer;
  connection;
  emittedError;
  isDisconnected = false;
  stream;
  pausingStream;
  #channel;
  #volume = 100;
  constructor(voiceManager, channel) {
    super();
    this.voices = voiceManager;
    this.id = channel.guildId;
    this.channel = channel;
    this.voices.add(this.id, this);
    this.audioPlayer = createAudioPlayer().on(AudioPlayerStatus.Idle, (oldState) => {
      if (oldState.status !== AudioPlayerStatus.Idle) this.emit("finish");
    }).on("error", (error) => {
      if (this.emittedError) return;
      this.emittedError = true;
      this.emit("error", error);
    });
    this.connection.on(VoiceConnectionStatus.Disconnected, (_, newState) => {
      if (newState.reason === VoiceConnectionDisconnectReason.Manual) {
        this.leave();
      } else if (newState.reason === VoiceConnectionDisconnectReason.WebSocketClose && newState.closeCode === 4014) {
        entersState(this.connection, VoiceConnectionStatus.Connecting, RECONNECT_TIMEOUT_MS).catch(() => {
          if (![VoiceConnectionStatus.Ready, VoiceConnectionStatus.Connecting].includes(this.connection.state.status)) {
            this.leave();
          }
        });
      } else if (this.connection.rejoinAttempts < RECONNECT_MAX_ATTEMPTS) {
        setTimeout(
          () => {
            this.connection.rejoin();
          },
          (this.connection.rejoinAttempts + 1) * RECONNECT_TIMEOUT_MS
        ).unref();
      } else if (this.connection.state.status !== VoiceConnectionStatus.Destroyed) {
        this.leave(new DisTubeError("VOICE_RECONNECT_FAILED"));
      }
    }).on(VoiceConnectionStatus.Destroyed, () => {
      this.leave();
    }).on("error", () => void 0);
    this.connection.subscribe(this.audioPlayer);
  }
  /**
   * The voice channel id the bot is in
   */
  get channelId() {
    return this.connection?.joinConfig?.channelId ?? void 0;
  }
  get channel() {
    if (!this.channelId) return this.#channel;
    if (this.#channel?.id === this.channelId) return this.#channel;
    const channel = this.voices.client.channels.cache.get(this.channelId);
    if (!channel) return this.#channel;
    for (const type of Constants.VoiceBasedChannelTypes) {
      if (channel.type === type) {
        this.#channel = channel;
        return channel;
      }
    }
    return this.#channel;
  }
  set channel(channel) {
    if (!isSupportedVoiceChannel(channel)) {
      throw new DisTubeError("INVALID_TYPE", "BaseGuildVoiceChannel", channel, "DisTubeVoice#channel");
    }
    if (channel.guildId !== this.id) throw new DisTubeError("VOICE_DIFFERENT_GUILD");
    if (channel.client.user?.id !== this.voices.client.user?.id) throw new DisTubeError("VOICE_DIFFERENT_CLIENT");
    if (channel.id === this.channelId) return;
    if (!channel.joinable) {
      if (channel.full) throw new DisTubeError("VOICE_FULL");
      else throw new DisTubeError("VOICE_MISSING_PERMS");
    }
    this.connection = this.#join(channel);
    this.#channel = channel;
  }
  #join(channel) {
    return joinVoiceChannel({
      channelId: channel.id,
      guildId: this.id,
      adapterCreator: channel.guild.voiceAdapterCreator,
      group: channel.client.user?.id
    });
  }
  /**
   * Join a voice channel with this connection
   * @param channel - A voice channel
   */
  async join(channel) {
    if (channel) this.channel = channel;
    try {
      await entersState(this.connection, VoiceConnectionStatus.Ready, JOIN_TIMEOUT_MS);
    } catch {
      if (this.connection.state.status === VoiceConnectionStatus.Ready) return this;
      if (this.connection.state.status !== VoiceConnectionStatus.Destroyed) this.connection.destroy();
      this.voices.remove(this.id);
      throw new DisTubeError("VOICE_CONNECT_FAILED", JOIN_TIMEOUT_MS / 1e3);
    }
    return this;
  }
  /**
   * Leave the voice channel of this connection
   * @param error - Optional, an error to emit with 'error' event.
   */
  leave(error) {
    this.stop(true);
    if (!this.isDisconnected) {
      this.emit("disconnect", error);
      this.isDisconnected = true;
    }
    if (this.connection.state.status !== VoiceConnectionStatus.Destroyed) this.connection.destroy();
    this.voices.remove(this.id);
  }
  /**
   * Stop the playing stream
   * @param force - If true, will force the {@link DisTubeVoice#audioPlayer} to enter the Idle state even
   *                if the {@link DisTubeStream#audioResource} has silence padding frames.
   */
  stop(force = false) {
    this.audioPlayer.stop(force);
  }
  #streamErrorHandler;
  /**
   * Play a {@link DisTubeStream}
   * @param dtStream - DisTubeStream
   */
  async play(dtStream) {
    if (!await checkEncryptionLibraries()) {
      dtStream.kill();
      throw new DisTubeError("ENCRYPTION_LIBRARIES_MISSING");
    }
    this.emittedError = false;
    if (this.stream && this.#streamErrorHandler) {
      this.stream.off("error", this.#streamErrorHandler);
    }
    this.#streamErrorHandler = (error) => {
      if (this.emittedError || error.code === "ERR_STREAM_PREMATURE_CLOSE") return;
      this.emittedError = true;
      this.emit("error", error);
    };
    dtStream.on("error", this.#streamErrorHandler);
    if (this.audioPlayer.state.status !== AudioPlayerStatus.Paused) {
      this.audioPlayer.play(dtStream.audioResource);
      this.stream?.kill();
      dtStream.spawn();
    } else if (!this.pausingStream) {
      this.pausingStream = this.stream;
    }
    this.stream = dtStream;
    this.volume = this.#volume;
  }
  set volume(volume) {
    if (typeof volume !== "number" || Number.isNaN(volume)) {
      throw new DisTubeError("INVALID_TYPE", "number", volume, "volume");
    }
    if (volume < 0) {
      throw new DisTubeError("NUMBER_COMPARE", "Volume", "bigger or equal to", 0);
    }
    this.#volume = volume;
    this.stream?.setVolume((this.#volume / 100) ** (0.5 / Math.log10(2)));
  }
  /**
   * Get or set the volume percentage
   */
  get volume() {
    return this.#volume;
  }
  /**
   * Playback duration of the audio resource in seconds (time since playback started)
   */
  get playbackDuration() {
    return (this.stream?.audioResource?.playbackDuration ?? 0) / 1e3;
  }
  /**
   * Current playback time in seconds, accounting for seek offset
   */
  get playbackTime() {
    return this.playbackDuration + (this.stream?.seekTime ?? 0);
  }
  pause() {
    this.audioPlayer.pause();
  }
  unpause() {
    const state = this.audioPlayer.state;
    if (state.status !== AudioPlayerStatus.Paused) return;
    if (this.stream?.audioResource && state.resource !== this.stream.audioResource) {
      this.audioPlayer.play(this.stream.audioResource);
      this.stream.spawn();
      this.pausingStream?.kill();
      delete this.pausingStream;
    } else {
      this.audioPlayer.unpause();
    }
  }
  /**
   * Whether the bot is self-deafened
   */
  get selfDeaf() {
    return this.connection.joinConfig.selfDeaf;
  }
  /**
   * Whether the bot is self-muted
   */
  get selfMute() {
    return this.connection.joinConfig.selfMute;
  }
  /**
   * Self-deafens/undeafens the bot.
   * @param selfDeaf - Whether or not the bot should be self-deafened
   * @returns true if the voice state was successfully updated, otherwise false
   */
  setSelfDeaf(selfDeaf) {
    if (typeof selfDeaf !== "boolean") {
      throw new DisTubeError("INVALID_TYPE", "boolean", selfDeaf, "selfDeaf");
    }
    return this.connection.rejoin({
      ...this.connection.joinConfig,
      selfDeaf
    });
  }
  /**
   * Self-mutes/unmutes the bot.
   * @param selfMute - Whether or not the bot should be self-muted
   * @returns true if the voice state was successfully updated, otherwise false
   */
  setSelfMute(selfMute) {
    if (typeof selfMute !== "boolean") {
      throw new DisTubeError("INVALID_TYPE", "boolean", selfMute, "selfMute");
    }
    return this.connection.rejoin({
      ...this.connection.joinConfig,
      selfMute
    });
  }
  /**
   * The voice state of this connection
   */
  get voiceState() {
    return this.channel?.guild?.members?.me?.voice;
  }
};

// src/core/manager/BaseManager.ts
import { Collection } from "discord.js";
var BaseManager = class extends DisTubeBase {
  static {
    __name(this, "BaseManager");
  }
  /**
   * The collection of items for this manager.
   */
  collection = new Collection();
  /**
   * The size of the collection.
   */
  get size() {
    return this.collection.size;
  }
};

// src/core/manager/FilterManager.ts
var FilterManager = class extends BaseManager {
  static {
    __name(this, "FilterManager");
  }
  /**
   * The queue to manage
   */
  queue;
  constructor(queue) {
    super(queue.distube);
    this.queue = queue;
  }
  #resolve(filter) {
    if (typeof filter === "object" && typeof filter.name === "string" && typeof filter.value === "string") {
      return filter;
    }
    if (typeof filter === "string" && Object.hasOwn(this.distube.filters, filter)) {
      return {
        name: filter,
        value: this.distube.filters[filter]
      };
    }
    throw new DisTubeError("INVALID_TYPE", "FilterResolvable", filter, "filter");
  }
  #apply() {
    this.queue._beginTime = this.queue.currentTime;
    this.queue.play(false);
  }
  /**
   * Enable a filter or multiple filters to the manager
   * @param filterOrFilters - The filter or filters to enable
   * @param override        - Wether or not override the applied filter with new filter value
   */
  add(filterOrFilters, override = false) {
    if (Array.isArray(filterOrFilters)) {
      for (const filter of filterOrFilters) {
        const ft = this.#resolve(filter);
        if (override || !this.has(ft)) this.collection.set(ft.name, ft);
      }
    } else {
      const ft = this.#resolve(filterOrFilters);
      if (override || !this.has(ft)) this.collection.set(ft.name, ft);
    }
    this.#apply();
    return this;
  }
  /**
   * Clear enabled filters of the manager
   */
  clear() {
    return this.set([]);
  }
  /**
   * Set the filters applied to the manager
   * @param filters - The filters to apply
   */
  set(filters) {
    if (!Array.isArray(filters)) throw new DisTubeError("INVALID_TYPE", "Array<FilterResolvable>", filters, "filters");
    this.collection.clear();
    for (const f of filters) {
      const filter = this.#resolve(f);
      this.collection.set(filter.name, filter);
    }
    this.#apply();
    return this;
  }
  #removeFn(f) {
    return this.collection.delete(this.#resolve(f).name);
  }
  /**
   * Disable a filter or multiple filters
   * @param filterOrFilters - The filter or filters to disable
   */
  remove(filterOrFilters) {
    if (Array.isArray(filterOrFilters)) filterOrFilters.forEach((f) => this.#removeFn(f));
    else this.#removeFn(filterOrFilters);
    this.#apply();
    return this;
  }
  /**
   * Check whether a filter enabled or not
   * @param filter - The filter to check
   */
  has(filter) {
    return this.collection.has(typeof filter === "string" ? filter : this.#resolve(filter).name);
  }
  /**
   * Array of enabled filter names
   */
  get names() {
    return [...this.collection.keys()];
  }
  /**
   * Array of enabled filters
   */
  get values() {
    return [...this.collection.values()];
  }
  get ffmpegArgs() {
    return this.size ? { af: this.values.map((f) => f.value).join(",") } : {};
  }
  toString() {
    return this.names.toString();
  }
};

// src/type.ts
var Events = /* @__PURE__ */ ((Events2) => {
  Events2["ERROR"] = "error";
  Events2["ADD_LIST"] = "addList";
  Events2["ADD_SONG"] = "addSong";
  Events2["PLAY_SONG"] = "playSong";
  Events2["FINISH_SONG"] = "finishSong";
  Events2["EMPTY"] = "empty";
  Events2["FINISH"] = "finish";
  Events2["INIT_QUEUE"] = "initQueue";
  Events2["NO_RELATED"] = "noRelated";
  Events2["DISCONNECT"] = "disconnect";
  Events2["DELETE_QUEUE"] = "deleteQueue";
  Events2["FFMPEG_DEBUG"] = "ffmpegDebug";
  Events2["DEBUG"] = "debug";
  return Events2;
})(Events || {});
var RepeatMode = /* @__PURE__ */ ((RepeatMode2) => {
  RepeatMode2[RepeatMode2["DISABLED"] = 0] = "DISABLED";
  RepeatMode2[RepeatMode2["SONG"] = 1] = "SONG";
  RepeatMode2[RepeatMode2["QUEUE"] = 2] = "QUEUE";
  return RepeatMode2;
})(RepeatMode || {});
var PluginType = /* @__PURE__ */ ((PluginType2) => {
  PluginType2["EXTRACTOR"] = "extractor";
  PluginType2["INFO_EXTRACTOR"] = "info-extractor";
  PluginType2["PLAYABLE_EXTRACTOR"] = "playable-extractor";
  return PluginType2;
})(PluginType || {});

// src/struct/TaskQueue.ts
var Task = class {
  static {
    __name(this, "Task");
  }
  resolve;
  promise;
  isPlay;
  constructor(isPlay) {
    this.isPlay = isPlay;
    this.promise = new Promise((res) => {
      this.resolve = res;
    });
  }
};
var TaskQueue = class {
  static {
    __name(this, "TaskQueue");
  }
  /**
   * The task array
   */
  #tasks = [];
  /**
   * Waits for last task finished and queues a new task
   */
  queuing(isPlay = false) {
    const next = this.remaining ? this.#tasks[this.#tasks.length - 1].promise : Promise.resolve();
    this.#tasks.push(new Task(isPlay));
    return next;
  }
  /**
   * Removes the finished task and processes the next task
   */
  resolve() {
    this.#tasks.shift()?.resolve();
  }
  /**
   * The remaining number of tasks
   */
  get remaining() {
    return this.#tasks.length;
  }
  /**
   * Whether or not having a play task
   */
  get hasPlayTask() {
    return this.#tasks.some((t) => t.isPlay);
  }
};

// src/struct/Queue.ts
var Queue = class extends DisTubeBase {
  static {
    __name(this, "Queue");
  }
  /**
   * Queue id (Guild id)
   */
  id;
  /**
   * Voice connection of this queue.
   */
  voice;
  /**
   * List of songs in the queue (The first one is the playing song)
   */
  songs;
  /**
   * List of the previous songs.
   */
  previousSongs;
  /**
   * Whether stream is currently stopped.
   */
  stopped;
  /**
   * Whether or not the queue is active.
   *
   * Note: This remains `true` when paused. It only becomes `false` when stopped.
   * @deprecated Use `!queue.paused` to check if audio is playing. Will be removed in v6.0.
   */
  playing;
  /**
   * Whether or not the stream is currently paused.
   */
  paused;
  /**
   * Type of repeat mode (`0` is disabled, `1` is repeating a song, `2` is repeating
   * all the queue). Default value: `0` (disabled)
   */
  repeatMode;
  /**
   * Whether or not the autoplay mode is enabled. Default value: `false`
   */
  autoplay;
  /**
   * FFmpeg arguments for the current queue. Default value is defined with {@link DisTubeOptions}.ffmpeg.args.
   * `af` output argument will be replaced with {@link Queue#filters} manager
   */
  ffmpegArgs;
  /**
   * The text channel of the Queue. (Default: where the first command is called).
   */
  textChannel;
  /**
   * What time in the song to begin (in seconds).
   * @internal
   */
  _beginTime;
  #filters;
  /**
   * Whether or not the queue is being updated manually (skip, jump, previous)
   * @internal
   */
  _manualUpdate;
  /**
   * Task queuing system
   * @internal
   */
  _taskQueue;
  /**
   * {@link DisTubeVoice} listener
   * @internal
   */
  _listeners;
  /**
   * Create a queue for the guild
   * @param distube     - DisTube
   * @param voice       - Voice connection
   * @param textChannel - Default text channel
   */
  constructor(distube, voice, textChannel) {
    super(distube);
    this.voice = voice;
    this.id = voice.id;
    this.volume = DEFAULT_VOLUME;
    this.songs = [];
    this.previousSongs = [];
    this.stopped = false;
    this._manualUpdate = false;
    this.playing = false;
    this.paused = false;
    this.repeatMode = 0 /* DISABLED */;
    this.autoplay = false;
    this.#filters = new FilterManager(this);
    this._beginTime = 0;
    this.textChannel = textChannel;
    this._taskQueue = new TaskQueue();
    this._listeners = void 0;
    this.ffmpegArgs = {
      global: { ...this.options.ffmpeg.args.global },
      input: { ...this.options.ffmpeg.args.input },
      output: { ...this.options.ffmpeg.args.output }
    };
  }
  #addToPreviousSongs(songs) {
    if (Array.isArray(songs)) {
      if (this.options.savePreviousSongs) {
        this.previousSongs.push(...songs);
      } else {
        this.previousSongs.push(...songs.map((s) => ({ id: s.id })));
      }
    } else if (this.options.savePreviousSongs) {
      this.previousSongs.push(songs);
    } else {
      this.previousSongs.push({ id: songs.id });
    }
  }
  #stop() {
    this._manualUpdate = true;
    this.voice.stop();
  }
  /**
   * The client user as a `GuildMember` of this queue's guild
   */
  get clientMember() {
    return this.voice.channel.guild.members.me ?? void 0;
  }
  /**
   * The filter manager of the queue
   */
  get filters() {
    return this.#filters;
  }
  /**
   * Formatted duration string.
   */
  get formattedDuration() {
    return formatDuration(this.duration);
  }
  /**
   * Queue's duration.
   */
  get duration() {
    return this.songs.length ? this.songs.reduce((prev, next) => prev + next.duration, 0) : 0;
  }
  /**
   * What time in the song is playing (in seconds).
   */
  get currentTime() {
    return this.voice.playbackTime;
  }
  /**
   * Formatted {@link Queue#currentTime} string.
   */
  get formattedCurrentTime() {
    return formatDuration(this.currentTime);
  }
  /**
   * The voice channel playing in.
   */
  get voiceChannel() {
    return this.clientMember?.voice?.channel ?? null;
  }
  /**
   * Get or set the stream volume. Default value: `50`.
   */
  get volume() {
    return this.voice.volume;
  }
  set volume(value) {
    this.voice.volume = value;
  }
  /**
   * @throws {DisTubeError}
   * @param song     - Song to add
   * @param position - Position to add, \<= 0 to add to the end of the queue
   * @returns The guild queue
   */
  addToQueue(song, position = 0) {
    if (this.stopped) throw new DisTubeError("QUEUE_STOPPED");
    if (!song || Array.isArray(song) && !song.length) {
      throw new DisTubeError("INVALID_TYPE", ["Song", "Array<Song>"], song, "song");
    }
    if (typeof position !== "number" || !Number.isInteger(position)) {
      throw new DisTubeError("INVALID_TYPE", "integer", position, "position");
    }
    if (position <= 0) {
      if (Array.isArray(song)) this.songs.push(...song);
      else this.songs.push(song);
    } else if (Array.isArray(song)) {
      this.songs.splice(position, 0, ...song);
    } else {
      this.songs.splice(position, 0, song);
    }
    return this;
  }
  /**
   * @returns `true` if the queue is active (not stopped)
   * @deprecated Use `!queue.paused` to check if audio is playing. Will be removed in v6.0.
   */
  isPlaying() {
    return this.playing;
  }
  /**
   * @returns `true` if the queue is paused
   * @deprecated Use `queue.paused` property instead. Will be removed in v6.0.
   */
  isPaused() {
    return this.paused;
  }
  /**
   * Pause the guild stream
   * @returns The guild queue
   */
  async pause() {
    await this._taskQueue.queuing();
    try {
      if (this.paused) throw new DisTubeError("PAUSED");
      this.paused = true;
      this.voice.pause();
      return this;
    } finally {
      this._taskQueue.resolve();
    }
  }
  /**
   * Resume the guild stream
   * @returns The guild queue
   */
  async resume() {
    await this._taskQueue.queuing();
    try {
      if (!this.paused) throw new DisTubeError("RESUMED");
      this.paused = false;
      this.voice.unpause();
      return this;
    } finally {
      this._taskQueue.resolve();
    }
  }
  /**
   * Set the guild stream's volume
   * @param percent - The percentage of volume you want to set
   * @returns The guild queue
   */
  setVolume(percent) {
    this.volume = percent;
    return this;
  }
  /**
   * Skip the playing song if there is a next song in the queue. <info>If {@link
   * Queue#autoplay} is `true` and there is no up next song, DisTube will add and
   * play a related song.</info>
   * @param options - Skip options
   * @returns The song will skip to
   */
  async skip(options) {
    return this.jump(1, options);
  }
  /**
   * Play the previous song if exists
   * @returns The guild queue
   */
  async previous() {
    await this._taskQueue.queuing();
    try {
      if (!this.options.savePreviousSongs) throw new DisTubeError("DISABLED_OPTION", "savePreviousSongs");
      if (this.previousSongs.length === 0 && this.repeatMode !== 2 /* QUEUE */) {
        throw new DisTubeError("NO_PREVIOUS");
      }
      const song = this.repeatMode === 2 /* QUEUE */ && this.previousSongs.length === 0 ? this.songs[this.songs.length - 1] : this.previousSongs.pop();
      this.songs.unshift(song);
      this.#stop();
      return song;
    } finally {
      this._taskQueue.resolve();
    }
  }
  /**
   * Shuffle the queue's songs
   * @returns The guild queue
   */
  async shuffle() {
    await this._taskQueue.queuing();
    try {
      const playing = this.songs.shift();
      if (playing === void 0) return this;
      for (let i = this.songs.length - 1; i > 0; i--) {
        const j = Math.floor(Math.random() * (i + 1));
        [this.songs[i], this.songs[j]] = [this.songs[j], this.songs[i]];
      }
      this.songs.unshift(playing);
      return this;
    } finally {
      this._taskQueue.resolve();
    }
  }
  /**
   * Jump to the song position in the queue. The next one is 1, 2,... The previous
   * one is -1, -2,...
   * if `num` is invalid number
   * @param position - The song position to play
   * @param options  - Skip options
   * @returns The new Song will be played
   */
  async jump(position, options) {
    await this._taskQueue.queuing();
    try {
      if (typeof position !== "number") throw new DisTubeError("INVALID_TYPE", "number", position, "position");
      if (!position || position > this.songs.length || -position > this.previousSongs.length) {
        throw new DisTubeError("NO_SONG_POSITION");
      }
      if (position > 0) {
        if (position >= this.songs.length) {
          if (this.autoplay) {
            await this._addRelatedSong();
          } else {
            throw new DisTubeError("NO_UP_NEXT");
          }
        }
        const skipped = this.songs.splice(0, position);
        if (options?.requeue) {
          this.songs.push(...skipped);
        } else {
          this.#addToPreviousSongs(skipped);
        }
      } else if (!this.options.savePreviousSongs) {
        throw new DisTubeError("DISABLED_OPTION", "savePreviousSongs");
      } else {
        const skipped = this.previousSongs.splice(position);
        this.songs.unshift(...skipped);
      }
      this.#stop();
      return this.songs[0];
    } finally {
      this._taskQueue.resolve();
    }
  }
  /**
   * Set the repeat mode of the guild queue.
   * Toggle mode `(Disabled -> Song -> Queue -> Disabled ->...)` if `mode` is `undefined`
   * @param mode - The repeat modes (toggle if `undefined`)
   * @returns The new repeat mode
   */
  setRepeatMode(mode) {
    if (mode !== void 0 && !Object.values(RepeatMode).includes(mode)) {
      throw new DisTubeError("INVALID_TYPE", ["RepeatMode", "undefined"], mode, "mode");
    }
    if (mode === void 0) this.repeatMode = (this.repeatMode + 1) % 3;
    else if (this.repeatMode === mode) this.repeatMode = 0 /* DISABLED */;
    else this.repeatMode = mode;
    return this.repeatMode;
  }
  /**
   * Set the playing time to another position
   * @param time - Time in seconds
   * @returns The guild queue
   */
  async seek(time) {
    await this._taskQueue.queuing();
    try {
      if (typeof time !== "number") throw new DisTubeError("INVALID_TYPE", "number", time, "time");
      if (Number.isNaN(time) || time < 0) throw new DisTubeError("NUMBER_COMPARE", "time", "bigger or equal to", 0);
      this._beginTime = time;
      await this.play(false);
      return this;
    } finally {
      this._taskQueue.resolve();
    }
  }
  async #getRelatedSong(current) {
    const plugin = await this.handler._getPluginFromSong(current);
    if (plugin) return plugin.getRelatedSongs(current);
    return [];
  }
  /**
   * Internal implementation of addRelatedSong without task queue protection.
   * Used by methods that already hold the task queue lock.
   * @internal
   */
  async _addRelatedSong(song) {
    const current = song ?? this.songs?.[0];
    if (!current) throw new DisTubeError("NO_PLAYING_SONG");
    const prevIds = this.previousSongs.map((p) => p.id);
    const relatedSongs = (await this.#getRelatedSong(current)).filter((s) => !prevIds.includes(s.id));
    this.debug(`[${this.id}] Getting related songs from: ${current}`);
    if (!relatedSongs.length && !current.stream.playFromSource) {
      const altSong = current.stream.song;
      if (altSong) relatedSongs.push(...(await this.#getRelatedSong(altSong)).filter((s) => !prevIds.includes(s.id)));
      this.debug(`[${this.id}] Getting related songs from streamed song: ${altSong}`);
    }
    const nextSong = relatedSongs[0];
    if (!nextSong) throw new DisTubeError("NO_RELATED");
    nextSong.metadata = current.metadata;
    nextSong.member = this.clientMember;
    this.addToQueue(nextSong);
    return nextSong;
  }
  /**
   * Add a related song of the playing song to the queue
   * @param song - The song to get related songs from. Defaults to the current playing song.
   * @returns The added song
   */
  async addRelatedSong(song) {
    await this._taskQueue.queuing();
    try {
      return await this._addRelatedSong(song);
    } finally {
      this._taskQueue.resolve();
    }
  }
  /**
   * Stop the guild stream and delete the queue
   */
  async stop() {
    await this._taskQueue.queuing();
    try {
      this.voice.stop();
      this.remove();
    } finally {
      this._taskQueue.resolve();
    }
  }
  /**
   * Remove the queue from the manager
   */
  remove() {
    this.playing = false;
    this.paused = true;
    this.stopped = true;
    this.songs = [];
    this.previousSongs = [];
    if (this._listeners) for (const event of objectKeys(this._listeners)) this.voice.off(event, this._listeners[event]);
    this.queues.remove(this.id);
    this.emit("deleteQueue" /* DELETE_QUEUE */, this);
  }
  /**
   * Toggle autoplay mode
   * @returns Autoplay mode state
   */
  toggleAutoplay() {
    this.autoplay = !this.autoplay;
    return this.autoplay;
  }
  /**
   * Play the first song in the queue
   * @param emitPlaySong - Whether or not emit {@link Events.PLAY_SONG} event
   */
  play(emitPlaySong = true) {
    if (this.stopped) throw new DisTubeError("QUEUE_STOPPED");
    this.playing = true;
    return this.queues.playSong(this, emitPlaySong);
  }
};

// src/util.ts
var formatInt = /* @__PURE__ */ __name((int) => int < 10 ? `0${int}` : int, "formatInt");
function formatDuration(sec) {
  if (!sec || !Number(sec)) return "00:00";
  const seconds = Math.floor(sec % 60);
  const minutes = Math.floor(sec % 3600 / 60);
  const hours = Math.floor(sec / 3600);
  if (hours > 0) return `${formatInt(hours)}:${formatInt(minutes)}:${formatInt(seconds)}`;
  if (minutes > 0) return `${formatInt(minutes)}:${formatInt(seconds)}`;
  return `00:${formatInt(seconds)}`;
}
__name(formatDuration, "formatDuration");
var SUPPORTED_PROTOCOL = ["https:", "http:", "file:"];
function isURL(input) {
  if (typeof input !== "string" || input.includes(" ")) return false;
  try {
    const url = new URL2(input);
    if (!SUPPORTED_PROTOCOL.some((p) => p === url.protocol)) return false;
  } catch {
    return false;
  }
  return true;
}
__name(isURL, "isURL");
function checkIntents(options) {
  const intents = options.intents instanceof IntentsBitField ? options.intents : new IntentsBitField(options.intents);
  if (!intents.has(GatewayIntentBits.GuildVoiceStates)) throw new DisTubeError("MISSING_INTENTS", "GuildVoiceStates");
}
__name(checkIntents, "checkIntents");
function isVoiceChannelEmpty(voiceState) {
  const guild = voiceState.guild;
  const clientId = voiceState.client.user?.id;
  if (!guild || !clientId) return false;
  const voiceChannel = guild.members.me?.voice?.channel;
  if (!voiceChannel) return false;
  const members = voiceChannel.members.filter((m) => !m.user.bot);
  return !members.size;
}
__name(isVoiceChannelEmpty, "isVoiceChannelEmpty");
function isSnowflake(id) {
  try {
    return SnowflakeUtil.deconstruct(id).timestamp > SnowflakeUtil.epoch;
  } catch {
    return false;
  }
}
__name(isSnowflake, "isSnowflake");
function isMemberInstance(member) {
  return Boolean(member) && isSnowflake(member.id) && isSnowflake(member.guild?.id) && isSnowflake(member.user?.id) && member.id === member.user.id;
}
__name(isMemberInstance, "isMemberInstance");
function isTextChannelInstance(channel) {
  return Boolean(channel) && isSnowflake(channel.id) && isSnowflake(channel.guildId || channel.guild?.id) && Constants2.TextBasedChannelTypes.includes(channel.type) && typeof channel.send === "function" && (typeof channel.nsfw === "boolean" || typeof channel.parent?.nsfw === "boolean");
}
__name(isTextChannelInstance, "isTextChannelInstance");
function isMessageInstance(message) {
  return Boolean(message) && isSnowflake(message.id) && isSnowflake(message.guildId || message.guild?.id) && isMemberInstance(message.member) && isTextChannelInstance(message.channel) && Constants2.NonSystemMessageTypes.includes(message.type) && message.member.id === message.author?.id;
}
__name(isMessageInstance, "isMessageInstance");
function isSupportedVoiceChannel(channel) {
  return Boolean(channel) && isSnowflake(channel.id) && isSnowflake(channel.guildId || channel.guild?.id) && Constants2.VoiceBasedChannelTypes.includes(channel.type);
}
__name(isSupportedVoiceChannel, "isSupportedVoiceChannel");
function isGuildInstance(guild) {
  return Boolean(guild) && isSnowflake(guild.id) && isSnowflake(guild.ownerId) && typeof guild.name === "string";
}
__name(isGuildInstance, "isGuildInstance");
function resolveGuildId(resolvable) {
  let guildId;
  if (typeof resolvable === "string") {
    guildId = resolvable;
  } else if (isObject(resolvable)) {
    if ("guildId" in resolvable && resolvable.guildId) {
      guildId = resolvable.guildId;
    } else if (resolvable instanceof Queue || resolvable instanceof DisTubeVoice || isGuildInstance(resolvable)) {
      guildId = resolvable.id;
    } else if ("guild" in resolvable && isGuildInstance(resolvable.guild)) {
      guildId = resolvable.guild.id;
    }
  }
  if (!isSnowflake(guildId)) throw new DisTubeError("INVALID_TYPE", "GuildIdResolvable", resolvable);
  return guildId;
}
__name(resolveGuildId, "resolveGuildId");
function isClientInstance(client) {
  return Boolean(client) && typeof client.login === "function";
}
__name(isClientInstance, "isClientInstance");
function checkInvalidKey(target, source, sourceName) {
  if (!isObject(target)) throw new DisTubeError("INVALID_TYPE", "object", target, sourceName);
  const sourceKeys = Array.isArray(source) ? source : objectKeys(source);
  const invalidKey = objectKeys(target).find((key) => !sourceKeys.includes(key));
  if (invalidKey) throw new DisTubeError("INVALID_KEY", sourceName, invalidKey);
}
__name(checkInvalidKey, "checkInvalidKey");
function isObject(obj) {
  return typeof obj === "object" && obj !== null && !Array.isArray(obj);
}
__name(isObject, "isObject");
function objectKeys(obj) {
  if (!isObject(obj)) return [];
  return Object.keys(obj);
}
__name(objectKeys, "objectKeys");
function isNsfwChannel(channel) {
  if (!isTextChannelInstance(channel)) return false;
  if (channel.isThread()) return channel.parent?.nsfw ?? false;
  return channel.nsfw;
}
__name(isNsfwChannel, "isNsfwChannel");
var isTruthy = /* @__PURE__ */ __name((x) => Boolean(x), "isTruthy");
var checkEncryptionLibraries = /* @__PURE__ */ __name(async () => {
  if (await import("crypto").then((m) => m.getCiphers().includes("aes-256-gcm"))) return true;
  for (const lib of [
    "@noble/ciphers",
    "@stablelib/xchacha20poly1305",
    "sodium-native",
    "sodium",
    "libsodium-wrappers",
    "tweetnacl"
  ]) {
    try {
      await import(lib);
      return true;
    } catch {
    }
  }
  return false;
}, "checkEncryptionLibraries");

// src/struct/Playlist.ts
var Playlist = class {
  static {
    __name(this, "Playlist");
  }
  /**
   * Playlist source.
   */
  source;
  /**
   * Songs in the playlist.
   */
  songs;
  /**
   * Playlist ID.
   */
  id;
  /**
   * Playlist name.
   */
  name;
  /**
   * Playlist URL.
   */
  url;
  /**
   * Playlist thumbnail.
   */
  thumbnail;
  #metadata;
  #member;
  /**
   * Create a Playlist
   * @param playlist  - Raw playlist info
   * @param options   - Optional data
   */
  constructor(playlist, { member, metadata } = {}) {
    if (!Array.isArray(playlist.songs) || !playlist.songs.length) throw new DisTubeError("EMPTY_PLAYLIST");
    this.source = playlist.source.toLowerCase();
    this.songs = playlist.songs;
    this.name = playlist.name;
    this.id = playlist.id;
    this.url = playlist.url;
    this.thumbnail = playlist.thumbnail;
    this.member = member;
    this.songs.forEach((s) => s.playlist = this);
    this.metadata = metadata;
  }
  /**
   * Playlist duration in second.
   */
  get duration() {
    return this.songs.reduce((prev, next) => prev + next.duration, 0);
  }
  /**
   * Formatted duration string `hh:mm:ss`.
   */
  get formattedDuration() {
    return formatDuration(this.duration);
  }
  /**
   * User requested.
   */
  get member() {
    return this.#member;
  }
  set member(member) {
    if (!isMemberInstance(member)) return;
    this.#member = member;
    this.songs.forEach((s) => s.member = this.member);
  }
  /**
   * User requested.
   */
  get user() {
    return this.member?.user;
  }
  /**
   * Optional metadata that can be used to identify the playlist.
   */
  get metadata() {
    return this.#metadata;
  }
  set metadata(metadata) {
    this.#metadata = metadata;
    this.songs.forEach((s) => s.metadata = metadata);
  }
  toString() {
    return `${this.name} (${this.songs.length} songs)`;
  }
};

// src/struct/Song.ts
var Song = class {
  static {
    __name(this, "Song");
  }
  /**
   * The source of this song info
   */
  source;
  /**
   * Song ID.
   */
  id;
  /**
   * Song name.
   */
  name;
  /**
   * Indicates if the song is an active live.
   */
  isLive;
  /**
   * Song duration.
   */
  duration;
  /**
   * Formatted duration string (`hh:mm:ss`, `mm:ss` or `Live`).
   */
  formattedDuration;
  /**
   * Song URL.
   */
  url;
  /**
   * Song thumbnail.
   */
  thumbnail;
  /**
   * Song view count
   */
  views;
  /**
   * Song like count
   */
  likes;
  /**
   * Song dislike count
   */
  dislikes;
  /**
   * Song repost (share) count
   */
  reposts;
  /**
   * Song uploader
   */
  uploader;
  /**
   * Whether or not an age-restricted content
   */
  ageRestricted;
  /**
   * Stream info
   */
  stream;
  /**
   * The plugin that created this song
   */
  plugin;
  #metadata;
  #member;
  #playlist;
  /**
   * Create a Song
   *
   * @param info      - Raw song info
   * @param options   - Optional data
   */
  constructor(info, { member, metadata } = {}) {
    this.source = info.source.toLowerCase();
    this.metadata = metadata;
    this.member = member;
    this.id = info.id;
    this.name = info.name;
    this.isLive = info.isLive;
    this.duration = this.isLive || !info.duration ? 0 : info.duration;
    this.formattedDuration = this.isLive ? "Live" : formatDuration(this.duration);
    this.url = info.url;
    this.thumbnail = info.thumbnail;
    this.views = info.views;
    this.likes = info.likes;
    this.dislikes = info.dislikes;
    this.reposts = info.reposts;
    this.uploader = {
      name: info.uploader?.name,
      url: info.uploader?.url
    };
    this.ageRestricted = info.ageRestricted;
    this.stream = { playFromSource: info.playFromSource };
    this.plugin = info.plugin;
  }
  /**
   * The playlist this song belongs to
   */
  get playlist() {
    return this.#playlist;
  }
  set playlist(playlist) {
    if (!(playlist instanceof Playlist)) throw new DisTubeError("INVALID_TYPE", "Playlist", playlist, "Song#playlist");
    this.#playlist = playlist;
    this.member = playlist.member;
  }
  /**
   * User requested to play this song.
   */
  get member() {
    return this.#member;
  }
  set member(member) {
    if (isMemberInstance(member)) this.#member = member;
  }
  /**
   * User requested to play this song.
   */
  get user() {
    return this.member?.user;
  }
  /**
   * Optional metadata that can be used to identify the song. This is attached by the
   * {@link DisTube#play} method.
   */
  get metadata() {
    return this.#metadata;
  }
  set metadata(metadata) {
    this.#metadata = metadata;
  }
  toString() {
    return this.name || this.url || this.id || "Unknown";
  }
};

// src/core/DisTubeHandler.ts
var DisTubeHandler = class extends DisTubeBase {
  static {
    __name(this, "DisTubeHandler");
  }
  /**
   * Resolve a url or a supported object to a {@link Song} or {@link Playlist}
   * @throws {@link DisTubeError}
   * @param input   - Resolvable input
   * @param options - Optional options
   * @returns Resolved
   */
  async resolve(input, options = {}) {
    if (input instanceof Song || input instanceof Playlist) {
      if ("metadata" in options) input.metadata = options.metadata;
      if ("member" in options) input.member = options.member;
      return input;
    }
    if (typeof input === "string") {
      if (isURL(input)) {
        const plugin = await this._getPluginFromURL(input) || await this._getPluginFromURL(input = await this.followRedirectLink(input));
        if (!plugin) throw new DisTubeError("NOT_SUPPORTED_URL");
        this.debug(`[${plugin.constructor.name}] Resolving from url: ${input}`);
        return plugin.resolve(input, options);
      }
      try {
        const song = await this.#searchSong(input, options);
        if (song) return song;
      } catch {
        throw new DisTubeError("NO_RESULT", input);
      }
    }
    throw new DisTubeError("CANNOT_RESOLVE_SONG", input);
  }
  async _getPluginFromURL(url) {
    for (const plugin of this.plugins) if (await plugin.validate(url)) return plugin;
    return null;
  }
  async _getPluginFromSong(song, types, validate = true) {
    if (!types || types.includes(song.plugin?.type)) return song.plugin;
    if (!song.url) return null;
    for (const plugin of this.plugins) {
      if ((!types || types.includes(plugin?.type)) && (!validate || await plugin.validate(song.url))) {
        return plugin;
      }
    }
    return null;
  }
  async #searchSong(query, options = {}, getStreamURL = false) {
    const plugins = this.plugins.filter((p) => p.type === "extractor" /* EXTRACTOR */);
    if (!plugins.length) throw new DisTubeError("NO_EXTRACTOR_PLUGIN");
    for (const plugin of plugins) {
      this.debug(`[${plugin.constructor.name}] Searching for song: ${query}`);
      const result = await plugin.searchSong(query, options);
      if (result) {
        if (getStreamURL && result.stream.playFromSource) result.stream.url = await plugin.getStreamURL(result);
        return result;
      }
    }
    return null;
  }
  /**
   * Get {@link Song}'s stream info and attach it to the song.
   * @param song - A Song
   */
  async attachStreamInfo(song) {
    if (song.stream.playFromSource) {
      if (song.stream.url) return;
      this.debug(`[DisTubeHandler] Getting stream info: ${song}`);
      const plugin = await this._getPluginFromSong(song, ["extractor" /* EXTRACTOR */, "playable-extractor" /* PLAYABLE_EXTRACTOR */]);
      if (!plugin) throw new DisTubeError("NOT_SUPPORTED_SONG", song.toString());
      this.debug(`[${plugin.constructor.name}] Getting stream URL: ${song}`);
      song.stream.url = await plugin.getStreamURL(song);
      if (!song.stream.url) throw new DisTubeError("CANNOT_GET_STREAM_URL", song.toString());
    } else {
      if (song.stream.song?.stream?.playFromSource && song.stream.song.stream.url) return;
      this.debug(`[DisTubeHandler] Getting stream info: ${song}`);
      const plugin = await this._getPluginFromSong(song, ["info-extractor" /* INFO_EXTRACTOR */]);
      if (!plugin) throw new DisTubeError("NOT_SUPPORTED_SONG", song.toString());
      this.debug(`[${plugin.constructor.name}] Creating search query for: ${song}`);
      const query = await plugin.createSearchQuery(song);
      if (!query) throw new DisTubeError("CANNOT_GET_SEARCH_QUERY", song.toString());
      const altSong = await this.#searchSong(query, { metadata: song.metadata, member: song.member }, true);
      if (!altSong || !altSong.stream.playFromSource) throw new DisTubeError("NO_RESULT", query || song.toString());
      song.stream.song = altSong;
    }
  }
  async followRedirectLink(url, maxRedirect = MAX_REDIRECT_DEPTH) {
    if (maxRedirect === 0) return url;
    const res = await request(url, {
      method: "HEAD",
      headers: {
        "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.3"
      }
    });
    if (HTTP_REDIRECT_CODES.has(res.statusCode ?? 200)) {
      let location = res.headers.location;
      if (typeof location !== "string") location = location?.[0] ?? url;
      return this.followRedirectLink(location, --maxRedirect);
    }
    return url;
  }
};

// src/core/DisTubeOptions.ts
var Options = class {
  static {
    __name(this, "Options");
  }
  plugins;
  emitNewSongOnly;
  savePreviousSongs;
  customFilters;
  nsfw;
  emitAddSongWhenCreatingQueue;
  emitAddListWhenCreatingQueue;
  joinNewVoiceChannel;
  ffmpeg;
  constructor(options) {
    if (typeof options !== "object" || Array.isArray(options)) {
      throw new DisTubeError("INVALID_TYPE", "object", options, "DisTubeOptions");
    }
    const opts = { ...defaultOptions, ...options };
    this.plugins = opts.plugins;
    this.emitNewSongOnly = opts.emitNewSongOnly;
    this.savePreviousSongs = opts.savePreviousSongs;
    this.customFilters = opts.customFilters;
    this.nsfw = opts.nsfw;
    this.emitAddSongWhenCreatingQueue = opts.emitAddSongWhenCreatingQueue;
    this.emitAddListWhenCreatingQueue = opts.emitAddListWhenCreatingQueue;
    this.joinNewVoiceChannel = opts.joinNewVoiceChannel;
    this.ffmpeg = this.#ffmpegOption(options);
    checkInvalidKey(opts, this, "DisTubeOptions");
    this.#validateOptions();
  }
  #validateOptions(options = this) {
    const booleanOptions = /* @__PURE__ */ new Set([
      "emitNewSongOnly",
      "savePreviousSongs",
      "joinNewVoiceChannel",
      "nsfw",
      "emitAddSongWhenCreatingQueue",
      "emitAddListWhenCreatingQueue"
    ]);
    const numberOptions = /* @__PURE__ */ new Set();
    const stringOptions = /* @__PURE__ */ new Set();
    const objectOptions = /* @__PURE__ */ new Set(["customFilters", "ffmpeg"]);
    const optionalOptions = /* @__PURE__ */ new Set(["customFilters"]);
    for (const [key, value] of Object.entries(options)) {
      if (value === void 0 && optionalOptions.has(key)) continue;
      if (key === "plugins" && !Array.isArray(value)) {
        throw new DisTubeError("INVALID_TYPE", "Array<Plugin>", value, `DisTubeOptions.${key}`);
      } else if (booleanOptions.has(key)) {
        if (typeof value !== "boolean") {
          throw new DisTubeError("INVALID_TYPE", "boolean", value, `DisTubeOptions.${key}`);
        }
      } else if (numberOptions.has(key)) {
        if (typeof value !== "number" || Number.isNaN(value)) {
          throw new DisTubeError("INVALID_TYPE", "number", value, `DisTubeOptions.${key}`);
        }
      } else if (stringOptions.has(key)) {
        if (typeof value !== "string") {
          throw new DisTubeError("INVALID_TYPE", "string", value, `DisTubeOptions.${key}`);
        }
      } else if (objectOptions.has(key)) {
        if (typeof value !== "object" || Array.isArray(value)) {
          throw new DisTubeError("INVALID_TYPE", "object", value, `DisTubeOptions.${key}`);
        }
      }
    }
  }
  #ffmpegOption(opts) {
    const args = { global: {}, input: {}, output: {} };
    if (opts.ffmpeg?.args) {
      if (opts.ffmpeg.args.global) args.global = opts.ffmpeg.args.global;
      if (opts.ffmpeg.args.input) args.input = opts.ffmpeg.args.input;
      if (opts.ffmpeg.args.output) args.output = opts.ffmpeg.args.output;
    }
    const path = opts.ffmpeg?.path ?? "ffmpeg";
    if (typeof path !== "string") {
      throw new DisTubeError("INVALID_TYPE", "string", path, "DisTubeOptions.ffmpeg.path");
    }
    for (const [key, value] of Object.entries(args)) {
      if (typeof value !== "object" || Array.isArray(value)) {
        throw new DisTubeError("INVALID_TYPE", "object", value, `DisTubeOptions.ffmpeg.${key}`);
      }
      for (const [k, v] of Object.entries(value)) {
        if (typeof v !== "string" && typeof v !== "number" && typeof v !== "boolean" && !Array.isArray(v) && v !== null && v !== void 0) {
          throw new DisTubeError(
            "INVALID_TYPE",
            ["string", "number", "boolean", "Array<string | null | undefined>", "null", "undefined"],
            v,
            `DisTubeOptions.ffmpeg.${key}.${k}`
          );
        }
      }
    }
    return { path, args };
  }
};

// src/core/DisTubeStream.ts
import { spawn, spawnSync } from "child_process";
import { Transform } from "stream";
import { createAudioResource, StreamType } from "@discordjs/voice";
import { TypedEmitter as TypedEmitter2 } from "tiny-typed-emitter";
var checked = process.env.NODE_ENV === "test";
var checkFFmpeg = /* @__PURE__ */ __name((distube) => {
  if (checked) return;
  const path = distube.options.ffmpeg.path;
  const debug = /* @__PURE__ */ __name((str) => distube.emit("ffmpegDebug" /* FFMPEG_DEBUG */, str), "debug");
  try {
    debug(`[test] spawn ffmpeg at '${path}' path`);
    const process2 = spawnSync(path, ["-h"], {
      windowsHide: true,
      encoding: "utf-8"
    });
    if (process2.error) throw process2.error;
    if (process2.stderr && !process2.stdout) throw new Error(process2.stderr);
    const result = process2.output.join("\n");
    const version2 = /ffmpeg version (\S+)/iu.exec(result)?.[1];
    if (!version2) throw new Error("Invalid FFmpeg version");
    debug(`[test] ffmpeg version: ${version2}`);
  } catch (e) {
    const errorMessage = e instanceof Error ? e.stack ?? e.message : String(e);
    debug(`[test] failed to spawn ffmpeg at '${path}': ${errorMessage}`);
    throw new DisTubeError("FFMPEG_NOT_INSTALLED", path);
  }
  checked = true;
}, "checkFFmpeg");
var DisTubeStream = class extends TypedEmitter2 {
  static {
    __name(this, "DisTubeStream");
  }
  #ffmpegPath;
  #opts;
  process;
  stream;
  audioResource;
  /**
   * The seek time in seconds that this stream started from
   */
  seekTime;
  /**
   * Create a DisTubeStream to play with {@link DisTubeVoice}
   * @param url     - Stream URL
   * @param options - Stream options
   */
  constructor(url, options) {
    super();
    const { ffmpeg, seek } = options;
    this.seekTime = typeof seek === "number" && seek > 0 ? seek : 0;
    const opts = {
      reconnect: 1,
      reconnect_streamed: 1,
      reconnect_delay_max: 5,
      analyzeduration: 0,
      hide_banner: true,
      ...ffmpeg.args.global,
      ...ffmpeg.args.input,
      i: url,
      ar: AUDIO_SAMPLE_RATE,
      ac: AUDIO_CHANNELS,
      ...ffmpeg.args.output,
      f: "s16le"
    };
    if (typeof seek === "number" && seek > 0) opts.ss = seek.toString();
    const fileUrl = new URL(url);
    if (fileUrl.protocol === "file:") {
      opts.reconnect = null;
      opts.reconnect_streamed = null;
      opts.reconnect_delay_max = null;
      opts.i = fileUrl.hostname + fileUrl.pathname;
    }
    this.#ffmpegPath = ffmpeg.path;
    this.#opts = [
      ...Object.entries(opts).flatMap(
        ([key, value]) => Array.isArray(value) ? value.filter(Boolean).map((v) => [`-${key}`, String(v)]) : value == null || value === false ? [] : [value === true ? `-${key}` : [`-${key}`, String(value)]]
      ).flat(),
      "pipe:1"
    ];
    this.stream = new VolumeTransformer();
    this.stream.on("close", () => this.kill()).on("error", (err) => {
      this.debug(`[stream] error: ${err.message}`);
      this.emit("error", err);
    }).on("finish", () => this.debug("[stream] log: stream finished"));
    this.audioResource = createAudioResource(this.stream, {
      inputType: StreamType.Raw,
      inlineVolume: false
    });
  }
  spawn() {
    this.debug(`[process] spawn: ${this.#ffmpegPath} ${this.#opts.join(" ")}`);
    this.process = spawn(this.#ffmpegPath, this.#opts, {
      stdio: ["ignore", "pipe", "pipe"],
      shell: false,
      windowsHide: true
    }).on("error", (err) => {
      this.debug(`[process] error: ${err.message}`);
      this.emit("error", err);
    }).on("exit", (code, signal) => {
      this.debug(`[process] exit: code=${code ?? "unknown"} signal=${signal ?? "unknown"}`);
      if (!code || [0, 255].includes(code)) return;
      this.debug(`[process] error: ffmpeg exited with code ${code}`);
      this.emit("error", new DisTubeError("FFMPEG_EXITED", code));
    });
    if (!this.process.stdout || !this.process.stderr) {
      this.kill();
      throw new Error("Failed to create ffmpeg process");
    }
    this.process.stdout.pipe(this.stream);
    this.process.stderr.setEncoding("utf8")?.on("data", (data) => {
      const lines = data.split(/\r\n|\r|\n/u);
      for (const line of lines) {
        if (/^\s*$/.test(line)) continue;
        this.debug(`[ffmpeg] log: ${line}`);
      }
    });
  }
  debug(debug) {
    this.emit("debug", debug);
  }
  setVolume(volume) {
    this.stream.vol = volume;
  }
  kill() {
    if (!this.stream.destroyed) this.stream.destroy();
    if (this.process && !this.process.killed) this.process.kill("SIGKILL");
  }
};
var VolumeTransformer = class extends Transform {
  static {
    __name(this, "VolumeTransformer");
  }
  buffer = Buffer.allocUnsafe(0);
  extrema = [-(2 ** (16 - 1)), 2 ** (16 - 1) - 1];
  vol = 1;
  _transform(newChunk, _encoding, done) {
    const { vol } = this;
    if (vol === 1) {
      this.push(newChunk);
      done();
      return;
    }
    const bytes = 2;
    const chunk = Buffer.concat([this.buffer, newChunk]);
    const readableLength = Math.floor(chunk.length / bytes) * bytes;
    for (let i = 0; i < readableLength; i += bytes) {
      const value = chunk.readInt16LE(i);
      const clampedValue = Math.min(this.extrema[1], Math.max(this.extrema[0], value * vol));
      chunk.writeInt16LE(clampedValue, i);
    }
    this.buffer = chunk.subarray(readableLength);
    this.push(chunk.subarray(0, readableLength));
    done();
  }
};

// src/core/manager/DisTubeVoiceManager.ts
import { getVoiceConnection, VoiceConnectionStatus as VoiceConnectionStatus2 } from "@discordjs/voice";

// src/core/manager/GuildIdManager.ts
var GuildIdManager = class extends BaseManager {
  static {
    __name(this, "GuildIdManager");
  }
  add(idOrInstance, data) {
    const id = resolveGuildId(idOrInstance);
    const existing = this.get(id);
    if (existing) return this;
    this.collection.set(id, data);
    return this;
  }
  get(idOrInstance) {
    return this.collection.get(resolveGuildId(idOrInstance));
  }
  remove(idOrInstance) {
    return this.collection.delete(resolveGuildId(idOrInstance));
  }
  has(idOrInstance) {
    return this.collection.has(resolveGuildId(idOrInstance));
  }
};

// src/core/manager/DisTubeVoiceManager.ts
var DisTubeVoiceManager = class extends GuildIdManager {
  static {
    __name(this, "DisTubeVoiceManager");
  }
  /**
   * Create a {@link DisTubeVoice} instance
   * @param channel - A voice channel to join
   */
  create(channel) {
    const existing = this.get(channel.guildId);
    if (existing) {
      existing.channel = channel;
      return existing;
    }
    if (getVoiceConnection(resolveGuildId(channel), this.client.user?.id) || getVoiceConnection(resolveGuildId(channel))) {
      throw new DisTubeError("VOICE_ALREADY_CREATED");
    }
    return new DisTubeVoice(this, channel);
  }
  /**
   * Join a voice channel and wait until the connection is ready
   * @param channel - A voice channel to join
   */
  join(channel) {
    const existing = this.get(channel.guildId);
    if (existing) return existing.join(channel);
    return this.create(channel).join();
  }
  /**
   * Leave the connected voice channel in a guild
   * @param guild - Queue Resolvable
   */
  leave(guild) {
    const voice = this.get(guild);
    if (voice) {
      voice.leave();
    } else {
      const connection = getVoiceConnection(resolveGuildId(guild), this.client.user?.id) ?? getVoiceConnection(resolveGuildId(guild));
      if (connection && connection.state.status !== VoiceConnectionStatus2.Destroyed) {
        connection.destroy();
      }
    }
  }
};

// src/core/manager/QueueManager.ts
var QueueManager = class extends GuildIdManager {
  static {
    __name(this, "QueueManager");
  }
  /**
   * Create a {@link Queue}
   * @param channel     - A voice channel
   * @param textChannel - Default text channel
   * @returns Returns `true` if encounter an error
   */
  async create(channel, textChannel) {
    if (this.has(channel.guildId)) throw new DisTubeError("QUEUE_EXIST");
    this.debug(`[QueueManager] Creating queue for guild: ${channel.guildId}`);
    const voice = this.voices.create(channel);
    const queue = new Queue(this.distube, voice, textChannel);
    await queue._taskQueue.queuing();
    try {
      checkFFmpeg(this.distube);
      this.debug(`[QueueManager] Joining voice channel: ${channel.id}`);
      await voice.join();
      this.#voiceEventHandler(queue);
      this.add(queue.id, queue);
      this.emit("initQueue" /* INIT_QUEUE */, queue);
      return queue;
    } finally {
      queue._taskQueue.resolve();
    }
  }
  /**
   * Listen to DisTubeVoice events and handle the Queue
   * @param queue - Queue
   */
  #voiceEventHandler(queue) {
    queue._listeners = {
      disconnect: /* @__PURE__ */ __name((error) => {
        queue.remove();
        this.emit("disconnect" /* DISCONNECT */, queue);
        if (error) this.emitError(error, queue, queue.songs?.[0]);
      }, "disconnect"),
      error: /* @__PURE__ */ __name((error) => void this.#handlePlayingError(queue, error), "error"),
      finish: /* @__PURE__ */ __name(() => void this.handleSongFinish(queue), "finish")
    };
    for (const event of objectKeys(queue._listeners)) {
      queue.voice.on(event, queue._listeners[event]);
    }
  }
  /**
   * Handle the queue when a Song finish
   * @param queue - queue
   */
  async handleSongFinish(queue) {
    if (queue._manualUpdate) {
      queue._manualUpdate = false;
      await this.playSong(queue);
      return;
    }
    this.debug(`[QueueManager] Handling song finish: ${queue.id}`);
    const song = queue.songs[0];
    this.emit("finishSong" /* FINISH_SONG */, queue, song);
    await queue._taskQueue.queuing();
    try {
      if (queue.stopped) return;
      if (queue.repeatMode === 2 /* QUEUE */) queue.songs.push(song);
      if (queue.repeatMode !== 1 /* SONG */) {
        const prev = queue.songs.shift();
        if (this.options.savePreviousSongs) queue.previousSongs.push(prev);
        else queue.previousSongs.push({ id: prev.id });
      }
      if (queue.songs.length === 0 && queue.autoplay) {
        try {
          this.debug(`[QueueManager] Adding related song: ${queue.id}`);
          await queue._addRelatedSong(song);
        } catch (e) {
          const errorMessage = e instanceof Error ? e.message : String(e);
          this.debug(`[${queue.id}] Add related song error: ${errorMessage}`);
          if (e instanceof DisTubeError) {
            this.emit("noRelated" /* NO_RELATED */, queue, e);
          } else {
            this.emit("noRelated" /* NO_RELATED */, queue, new DisTubeError("NO_RELATED"));
          }
        }
      }
      if (queue.songs.length === 0) {
        this.debug(`[${queue.id}] Queue is empty, stopping...`);
        if (!queue.autoplay) this.emit("finish" /* FINISH */, queue);
        queue.remove();
        return;
      }
      if (song !== queue.songs[0]) {
        const playedSong = song.stream.playFromSource ? song : song.stream.song;
        if (playedSong?.stream.playFromSource) delete playedSong.stream.url;
      }
      await this.playSong(queue, true);
    } finally {
      queue._taskQueue.resolve();
    }
  }
  /**
   * Handle error while playing
   * @param queue - queue
   * @param error - error
   */
  async #handlePlayingError(queue, error) {
    const song = queue.songs.shift();
    try {
      error.name = "PlayingError";
    } catch {
    }
    this.debug(`[${queue.id}] Error while playing: ${error.stack || error.message}`);
    this.emitError(error, queue, song);
    if (queue.songs.length > 0) {
      this.debug(`[${queue.id}] Playing next song: ${queue.songs[0]}`);
      await this.playSong(queue);
    } else {
      this.debug(`[${queue.id}] Queue is empty, stopping...`);
      await queue.stop();
    }
  }
  /**
   * Play a song on voice connection with queue properties
   * @param queue         - The guild queue to play
   * @param emitPlaySong  - Whether or not emit {@link Events.PLAY_SONG} event
   */
  async playSong(queue, emitPlaySong = true) {
    if (!queue) return;
    if (queue.stopped || !queue.songs.length) {
      await queue.stop();
      return;
    }
    try {
      const song = queue.songs[0];
      this.debug(`[${queue.id}] Getting stream from: ${song}`);
      await this.handler.attachStreamInfo(song);
      const willPlaySong = song.stream.playFromSource ? song : song.stream.song;
      const stream = willPlaySong?.stream;
      if (!willPlaySong || !stream?.playFromSource || !stream.url) throw new DisTubeError("NO_STREAM_URL", `${song}`);
      this.debug(`[${queue.id}] Creating DisTubeStream for: ${willPlaySong}`);
      const streamOptions = {
        ffmpeg: {
          path: this.options.ffmpeg.path,
          args: {
            global: { ...queue.ffmpegArgs.global },
            input: { ...queue.ffmpegArgs.input },
            output: { ...queue.ffmpegArgs.output, ...queue.filters.ffmpegArgs }
          }
        },
        seek: willPlaySong.duration ? queue._beginTime : void 0
      };
      queue._beginTime = 0;
      const dtStream = new DisTubeStream(stream.url, streamOptions);
      dtStream.on("debug", (data) => this.emit("ffmpegDebug" /* FFMPEG_DEBUG */, `[${queue.id}] ${data}`));
      this.debug(`[${queue.id}] Started playing: ${willPlaySong}`);
      await queue.voice.play(dtStream);
      if (emitPlaySong) this.emit("playSong" /* PLAY_SONG */, queue, song);
    } catch (e) {
      const error = e instanceof Error ? e : new Error(String(e));
      this.#handlePlayingError(queue, error);
    }
  }
};

// src/DisTube.ts
import { TypedEmitter as TypedEmitter3 } from "tiny-typed-emitter";
var DisTube = class extends TypedEmitter3 {
  static {
    __name(this, "DisTube");
  }
  /**
   * @event
   * Emitted after DisTube add a new playlist to the playing {@link Queue}.
   * @param queue    - The guild queue
   * @param playlist - Playlist info
   */
  static ["addList" /* ADD_LIST */];
  /**
   * @event
   * Emitted after DisTube add a new song to the playing {@link Queue}.
   * @param queue - The guild queue
   * @param song  - Added song
   */
  static ["addSong" /* ADD_SONG */];
  /**
   * @event
   * Emitted when a {@link Queue} is deleted with any reasons.
   * @param queue - The guild queue
   */
  static ["deleteQueue" /* DELETE_QUEUE */];
  /**
   * @event
   * Emitted when the bot is disconnected to a voice channel.
   * @param queue - The guild queue
   */
  static ["disconnect" /* DISCONNECT */];
  /**
   * @event
   * Emitted when DisTube encounters an error while playing songs.
   * @param error   - error
   * @param queue   - The queue encountered the error
   * @param song    - The playing song when encountered the error
   */
  static ["error" /* ERROR */];
  /**
   * @event
   * Emitted for logging FFmpeg debug information.
   * @param debug - Debug message string.
   */
  static ["ffmpegDebug" /* FFMPEG_DEBUG */];
  /**
   * @event
   * Emitted to provide debug information from DisTube's operation.
   * Useful for troubleshooting or logging purposes.
   *
   * @param debug - Debug message string.
   */
  static ["debug" /* DEBUG */];
  /**
   * @event
   * Emitted when there is no more song in the queue and {@link Queue#autoplay} is `false`.
   * @param queue - The guild queue
   */
  static ["finish" /* FINISH */];
  /**
   * @event
   * Emitted when DisTube finished a song.
   * @param queue - The guild queue
   * @param song  - Finished song
   */
  static ["finishSong" /* FINISH_SONG */];
  /**
   * @event
   * Emitted when DisTube initialize a queue to change queue default properties.
   * @param queue - The guild queue
   */
  static ["initQueue" /* INIT_QUEUE */];
  /**
   * @event
   * Emitted when {@link Queue#autoplay} is `true`, {@link Queue#songs} is empty, and
   * DisTube cannot find related songs to play.
   * @param queue - The guild queue
   */
  static ["noRelated" /* NO_RELATED */];
  /**
   * @event
   * Emitted when DisTube play a song.
   * If {@link DisTubeOptions}.emitNewSongOnly is `true`, this event is not emitted
   * when looping a song or next song is the previous one.
   * @param queue - The guild queue
   * @param song  - Playing song
   */
  static ["playSong" /* PLAY_SONG */];
  /**
   * DisTube internal handler
   */
  handler;
  /**
   * DisTube options
   */
  options;
  /**
   * Discord.js v14 client
   */
  client;
  /**
   * Queues manager
   */
  queues;
  /**
   * DisTube voice connections manager
   */
  voices;
  /**
   * DisTube plugins
   */
  plugins;
  /**
   * DisTube ffmpeg audio filters
   */
  filters;
  /**
   * Create a new DisTube class.
   * @throws {@link DisTubeError}
   * @param client - Discord.JS client
   * @param opts   - Custom DisTube options
   */
  constructor(client, opts = {}) {
    super();
    this.setMaxListeners(1);
    if (!isClientInstance(client)) throw new DisTubeError("INVALID_TYPE", "Discord.Client", client, "client");
    this.client = client;
    checkIntents(client.options);
    this.options = new Options(opts);
    this.voices = new DisTubeVoiceManager(this);
    this.handler = new DisTubeHandler(this);
    this.queues = new QueueManager(this);
    this.filters = { ...defaultFilters, ...this.options.customFilters };
    this.plugins = [...this.options.plugins];
    for (const p of this.plugins) p.init(this);
  }
  static get version() {
    return version;
  }
  /**
   * DisTube version
   */
  get version() {
    return version;
  }
  /**
   * Play / add a song or playlist from url.
   * Search and play a song (with {@link ExtractorPlugin}) if it is not a valid url.
   * @throws {@link DisTubeError}
   * @param voiceChannel - The channel will be joined if the bot isn't in any channels, the bot will be
   *                       moved to this channel if {@link DisTubeOptions}.joinNewVoiceChannel is `true`
   * @param song         - URL | Search string | {@link Song} | {@link Playlist}
   * @param options      - Optional options
   */
  async play(voiceChannel, song, options = {}) {
    if (!isSupportedVoiceChannel(voiceChannel)) {
      throw new DisTubeError("INVALID_TYPE", "BaseGuildVoiceChannel", voiceChannel, "voiceChannel");
    }
    if (!isObject(options)) throw new DisTubeError("INVALID_TYPE", "object", options, "options");
    const { textChannel, member, skip, message, metadata } = {
      member: voiceChannel.guild.members.me ?? void 0,
      textChannel: options?.message?.channel,
      skip: false,
      ...options
    };
    const position = Number(options.position) || (skip ? 1 : 0);
    if (message && !isMessageInstance(message)) {
      throw new DisTubeError("INVALID_TYPE", ["Discord.Message", "a falsy value"], message, "options.message");
    }
    if (textChannel && !isTextChannelInstance(textChannel)) {
      throw new DisTubeError("INVALID_TYPE", "Discord.GuildTextBasedChannel", textChannel, "options.textChannel");
    }
    if (member && !isMemberInstance(member)) {
      throw new DisTubeError("INVALID_TYPE", "Discord.GuildMember", member, "options.member");
    }
    const queue = this.getQueue(voiceChannel) || await this.queues.create(voiceChannel, textChannel);
    await queue._taskQueue.queuing(true);
    try {
      this.debug(`[${queue.id}] Playing input: ${song}`);
      const resolved = await this.handler.resolve(song, { member, metadata });
      const isNsfw = isNsfwChannel(queue?.textChannel || textChannel);
      const isFirstSong = queue.songs.length === 0;
      if (resolved instanceof Playlist) {
        if (!this.options.nsfw && !isNsfw) {
          resolved.songs = resolved.songs.filter((s) => !s.ageRestricted);
          if (!resolved.songs.length) throw new DisTubeError("EMPTY_FILTERED_PLAYLIST");
        }
        if (!resolved.songs.length) throw new DisTubeError("EMPTY_PLAYLIST");
        this.debug(`[${queue.id}] Adding playlist to queue: ${resolved.songs.length} songs`);
        queue.addToQueue(resolved.songs, position);
        if (!isFirstSong || this.options.emitAddListWhenCreatingQueue) this.emit("addList" /* ADD_LIST */, queue, resolved);
      } else {
        if (!this.options.nsfw && resolved.ageRestricted && !isNsfwChannel(queue?.textChannel || textChannel)) {
          throw new DisTubeError("NON_NSFW");
        }
        this.debug(`[${queue.id}] Adding song to queue: ${resolved.name || resolved.url || resolved.id || resolved}`);
        queue.addToQueue(resolved, position);
        if (!isFirstSong || this.options.emitAddSongWhenCreatingQueue) this.emit("addSong" /* ADD_SONG */, queue, resolved);
      }
      if (isFirstSong) await queue.play();
      else if (skip) await queue.skip();
    } catch (e) {
      if (!(e instanceof DisTubeError)) {
        const errorMessage = e instanceof Error ? e.stack ?? e.message : String(e);
        this.debug(`[${queue.id}] Unexpected error while playing song: ${errorMessage}`);
        if (e instanceof Error) {
          try {
            e.name = "PlayError";
            e.message = `${typeof song === "string" ? song : song.url}
${e.message}`;
          } catch {
          }
        }
      }
      throw e;
    } finally {
      if (!queue.songs.length && !queue._taskQueue.hasPlayTask) queue.remove();
      queue._taskQueue.resolve();
    }
  }
  /**
   * Create a custom playlist
   * @param songs   - Array of url or Song
   * @param options - Optional options
   */
  async createCustomPlaylist(songs, { member, parallel, metadata, name, source, url, thumbnail } = {}) {
    if (!Array.isArray(songs)) throw new DisTubeError("INVALID_TYPE", "Array", songs, "songs");
    if (!songs.length) throw new DisTubeError("EMPTY_ARRAY", "songs");
    const filteredSongs = songs.filter((song) => song instanceof Song || isURL(song));
    if (!filteredSongs.length) throw new DisTubeError("NO_VALID_SONG");
    if (member && !isMemberInstance(member)) {
      throw new DisTubeError("INVALID_TYPE", "Discord.Member", member, "options.member");
    }
    let resolvedSongs;
    if (parallel !== false) {
      const promises = filteredSongs.map(
        (song) => this.handler.resolve(song, { member, metadata }).catch(() => void 0)
      );
      resolvedSongs = (await Promise.all(promises)).filter((s) => s instanceof Song);
    } else {
      resolvedSongs = [];
      for (const song of filteredSongs) {
        const resolved = await this.handler.resolve(song, { member, metadata }).catch(() => void 0);
        if (resolved instanceof Song) resolvedSongs.push(resolved);
      }
    }
    return new Playlist(
      {
        source: source || "custom",
        name,
        url,
        thumbnail: thumbnail || resolvedSongs.find((s) => s.thumbnail)?.thumbnail,
        songs: resolvedSongs
      },
      { member, metadata }
    );
  }
  /**
   * Get the guild queue
   * @param guild - The type can be resolved to give a {@link Queue}
   */
  getQueue(guild) {
    return this.queues.get(guild);
  }
  #getQueue(guild) {
    const queue = this.getQueue(guild);
    if (!queue) throw new DisTubeError("NO_QUEUE");
    return queue;
  }
  /**
   * Pause the guild stream
   * @param guild - The type can be resolved to give a {@link Queue}
   * @returns The guild queue
   * @deprecated Use `distube.getQueue(guild).pause()` instead. Will be removed in v6.0.
   */
  pause(guild) {
    return this.#getQueue(guild).pause();
  }
  /**
   * Resume the guild stream
   * @param guild - The type can be resolved to give a {@link Queue}
   * @returns The guild queue
   * @deprecated Use `distube.getQueue(guild).resume()` instead. Will be removed in v6.0.
   */
  resume(guild) {
    return this.#getQueue(guild).resume();
  }
  /**
   * Stop the guild stream
   * @param guild - The type can be resolved to give a {@link Queue}
   * @deprecated Use `distube.getQueue(guild).stop()` instead. Will be removed in v6.0.
   */
  stop(guild) {
    return this.#getQueue(guild).stop();
  }
  /**
   * Set the guild stream's volume
   * @param guild   - The type can be resolved to give a {@link Queue}
   * @param percent - The percentage of volume you want to set
   * @returns The guild queue
   * @deprecated Use `distube.getQueue(guild).setVolume(percent)` instead. Will be removed in v6.0.
   */
  setVolume(guild, percent) {
    return this.#getQueue(guild).setVolume(percent);
  }
  /**
   * Skip the playing song if there is a next song in the queue. <info>If {@link
   * Queue#autoplay} is `true` and there is no up next song, DisTube will add and
   * play a related song.</info>
   * @param guild - The type can be resolved to give a {@link Queue}
   * @returns The new Song will be played
   * @deprecated Use `distube.getQueue(guild).skip(options)` instead. Will be removed in v6.0.
   */
  skip(guild, options) {
    return this.#getQueue(guild).skip(options);
  }
  /**
   * Play the previous song
   * @param guild - The type can be resolved to give a {@link Queue}
   * @returns The new Song will be played
   * @deprecated Use `distube.getQueue(guild).previous()` instead. Will be removed in v6.0.
   */
  previous(guild) {
    return this.#getQueue(guild).previous();
  }
  /**
   * Shuffle the guild queue songs
   * @param guild - The type can be resolved to give a {@link Queue}
   * @returns The guild queue
   * @deprecated Use `distube.getQueue(guild).shuffle()` instead. Will be removed in v6.0.
   */
  shuffle(guild) {
    return this.#getQueue(guild).shuffle();
  }
  /**
   * Jump to the song number in the queue. The next one is 1, 2,... The previous one
   * is -1, -2,...
   * @param guild - The type can be resolved to give a {@link Queue}
   * @param num   - The song number to play
   * @returns The new Song will be played
   * @deprecated Use `distube.getQueue(guild).jump(num, options)` instead. Will be removed in v6.0.
   */
  jump(guild, num, options) {
    return this.#getQueue(guild).jump(num, options);
  }
  /**
   * Set the repeat mode of the guild queue.
   * Toggle mode `(Disabled -> Song -> Queue -> Disabled ->...)` if `mode` is `undefined`
   * @param guild - The type can be resolved to give a {@link Queue}
   * @param mode  - The repeat modes (toggle if `undefined`)
   * @returns The new repeat mode
   * @deprecated Use `distube.getQueue(guild).setRepeatMode(mode)` instead. Will be removed in v6.0.
   */
  setRepeatMode(guild, mode) {
    return this.#getQueue(guild).setRepeatMode(mode);
  }
  /**
   * Toggle autoplay mode
   * @param guild - The type can be resolved to give a {@link Queue}
   * @returns Autoplay mode state
   * @deprecated Use `distube.getQueue(guild).toggleAutoplay()` instead. Will be removed in v6.0.
   */
  toggleAutoplay(guild) {
    const queue = this.#getQueue(guild);
    queue.autoplay = !queue.autoplay;
    return queue.autoplay;
  }
  /**
   * Add related song to the queue
   * @param guild - The type can be resolved to give a {@link Queue}
   * @returns The guild queue
   * @deprecated Use `distube.getQueue(guild).addRelatedSong()` instead. Will be removed in v6.0.
   */
  addRelatedSong(guild) {
    return this.#getQueue(guild).addRelatedSong();
  }
  /**
   * Set the playing time to another position
   * @param guild - The type can be resolved to give a {@link Queue}
   * @param time  - Time in seconds
   * @returns Seeked queue
   * @deprecated Use `distube.getQueue(guild).seek(time)` instead. Will be removed in v6.0.
   */
  seek(guild, time) {
    return this.#getQueue(guild).seek(time);
  }
  /**
   * Emit error event
   * @param error   - error
   * @param queue   - The queue encountered the error
   * @param song    - The playing song when encountered the error
   */
  emitError(error, queue, song) {
    this.emit("error" /* ERROR */, error, queue, song);
  }
  /**
   * Emit debug event
   * @param message - debug message
   */
  debug(message) {
    this.emit("debug" /* DEBUG */, message);
  }
};

// src/struct/Plugin.ts
var Plugin = class {
  static {
    __name(this, "Plugin");
  }
  /**
   * DisTube
   */
  distube;
  init(distube) {
    this.distube = distube;
  }
};

// src/struct/ExtractorPlugin.ts
var ExtractorPlugin = class extends Plugin {
  static {
    __name(this, "ExtractorPlugin");
  }
  type = "extractor" /* EXTRACTOR */;
};

// src/struct/InfoExtractorPlugin.ts
var InfoExtractorPlugin = class extends Plugin {
  static {
    __name(this, "InfoExtractorPlugin");
  }
  type = "info-extractor" /* INFO_EXTRACTOR */;
};

// src/struct/PlayableExtractorPlugin.ts
var PlayableExtractorPlugin = class extends Plugin {
  static {
    __name(this, "PlayableExtractorPlugin");
  }
  type = "playable-extractor" /* PLAYABLE_EXTRACTOR */;
};
export {
  AUDIO_CHANNELS,
  AUDIO_SAMPLE_RATE,
  BaseManager,
  DEFAULT_VOLUME,
  DisTube,
  DisTubeBase,
  DisTubeError,
  DisTubeHandler,
  DisTubeStream,
  DisTubeVoice,
  DisTubeVoiceManager,
  Events,
  ExtractorPlugin,
  FilterManager,
  GuildIdManager,
  HTTP_REDIRECT_CODES,
  InfoExtractorPlugin,
  InfoExtractorPlugin as InfoExtratorPlugin,
  JOIN_TIMEOUT_MS,
  MAX_REDIRECT_DEPTH,
  Options,
  PlayableExtractorPlugin,
  PlayableExtractorPlugin as PlayableExtratorPlugin,
  Playlist,
  Plugin,
  PluginType,
  Queue,
  QueueManager,
  RECONNECT_MAX_ATTEMPTS,
  RECONNECT_TIMEOUT_MS,
  RepeatMode,
  Song,
  TaskQueue,
  checkEncryptionLibraries,
  checkFFmpeg,
  checkIntents,
  checkInvalidKey,
  DisTube as default,
  defaultFilters,
  defaultOptions,
  formatDuration,
  isClientInstance,
  isGuildInstance,
  isMemberInstance,
  isMessageInstance,
  isNsfwChannel,
  isObject,
  isSnowflake,
  isSupportedVoiceChannel,
  isTextChannelInstance,
  isTruthy,
  isURL,
  isVoiceChannelEmpty,
  objectKeys,
  resolveGuildId,
  version
};
//# sourceMappingURL=index.mjs.map