2021-07-04 18:12:27 +05:00
import { Collection , Guild , StageChannel , VoiceChannel , Snowflake , SnowflakeUtil , GuildChannelResolvable } from "discord.js" ;
2021-06-11 15:32:22 +05:00
import { Player } from "../Player" ;
2021-06-25 12:24:53 +05:00
import { StreamDispatcher } from "../VoiceInterface/StreamDispatcher" ;
2021-06-11 15:32:22 +05:00
import Track from "./Track" ;
2021-06-24 10:20:12 +05:00
import { PlayerOptions , PlayerProgressbarOptions , PlayOptions , QueueFilters , QueueRepeatMode } from "../types/types" ;
2021-06-11 23:19:52 +05:00
import ytdl from "discord-ytdl-core" ;
import { AudioResource , StreamType } from "@discordjs/voice" ;
2021-06-13 17:43:36 +05:00
import { Util } from "../utils/Util" ;
2021-06-13 18:09:25 +05:00
import YouTube from "youtube-sr" ;
2021-06-19 23:10:38 +05:00
import AudioFilters from "../utils/AudioFilters" ;
2021-08-07 19:25:21 +05:00
import { PlayerError , ErrorStatusCode } from "./PlayerError" ;
2021-06-11 15:32:22 +05:00
2021-06-12 11:37:41 +05:00
class Queue < T = unknown > {
2021-06-11 15:32:22 +05:00
public readonly guild : Guild ;
public readonly player : Player ;
2021-06-11 23:19:52 +05:00
public connection : StreamDispatcher ;
2021-06-11 15:32:22 +05:00
public tracks : Track [ ] = [ ] ;
2021-06-13 17:43:36 +05:00
public previousTracks : Track [ ] = [ ] ;
2021-06-11 15:32:22 +05:00
public options : PlayerOptions ;
2021-06-11 23:19:52 +05:00
public playing = false ;
2021-06-12 11:37:41 +05:00
public metadata? : T = null ;
2021-06-13 17:43:36 +05:00
public repeatMode : QueueRepeatMode = 0 ;
2021-06-26 00:14:07 +05:00
public readonly id : Snowflake = SnowflakeUtil . generate ( ) ;
2021-06-22 15:24:05 +05:00
private _streamTime = 0 ;
2021-06-13 21:47:04 +05:00
public _cooldownsTimeout = new Collection < string , NodeJS.Timeout > ( ) ;
2021-06-22 15:24:05 +05:00
private _activeFilters : any [ ] = [ ] ; // eslint-disable-line @typescript-eslint/no-explicit-any
2021-06-22 10:33:20 +05:00
private _filtersUpdate = false ;
2021-06-26 11:40:24 +05:00
# lastVolume = 0 ;
2021-06-24 10:24:34 +05:00
# destroyed = false ;
2021-06-11 15:32:22 +05:00
2021-06-14 22:51:54 +05:00
/ * *
* Queue constructor
* @param { Player } player The player that instantiated this queue
* @param { Guild } guild The guild that instantiated this queue
2021-06-20 19:22:09 +05:00
* @param { PlayerOptions } [ options = { } ] Player options for the queue
2021-06-14 22:51:54 +05:00
* /
2021-06-11 15:32:22 +05:00
constructor ( player : Player , guild : Guild , options : PlayerOptions = { } ) {
2021-06-14 22:51:54 +05:00
/ * *
* The player that instantiated this queue
* @type { Player }
2021-06-20 19:22:09 +05:00
* @readonly
2021-06-14 22:51:54 +05:00
* /
2021-06-11 15:32:22 +05:00
this . player = player ;
2021-06-14 22:51:54 +05:00
/ * *
* The guild that instantiated this queue
* @type { Guild }
2021-06-20 19:22:09 +05:00
* @readonly
2021-06-14 22:51:54 +05:00
* /
2021-06-11 15:32:22 +05:00
this . guild = guild ;
2021-06-14 22:51:54 +05:00
/ * *
* The player options for this queue
* @type { PlayerOptions }
* /
2021-06-11 15:32:22 +05:00
this . options = { } ;
2021-06-22 10:33:20 +05:00
/ * *
* Queue repeat mode
* @type { QueueRepeatMode }
* @name Queue # repeatMode
* /
/ * *
* Queue metadata
* @type { any }
* @name Queue # metadata
* /
/ * *
* Previous tracks
* @type { Track [ ] }
* @name Queue # previousTracks
* /
/ * *
* Regular tracks
* @type { Track [ ] }
* @name Queue # tracks
* /
/ * *
* The connection
* @type { StreamDispatcher }
* @name Queue # connection
* /
2021-06-26 00:14:07 +05:00
/ * *
* The ID of this queue
* @type { Snowflake }
* @name Queue # id
* /
2021-06-11 15:32:22 +05:00
Object . assign (
this . options ,
{
leaveOnEnd : true ,
leaveOnStop : true ,
leaveOnEmpty : true ,
leaveOnEmptyCooldown : 1000 ,
autoSelfDeaf : true ,
2021-08-05 14:24:58 +05:00
ytdlOptions : {
highWaterMark : 1 << 25
} ,
2021-07-21 10:27:40 +05:00
initialVolume : 100 ,
2021-08-07 22:16:44 +05:00
bufferingTimeout : 3000
2021-06-11 15:32:22 +05:00
} as PlayerOptions ,
options
) ;
2021-08-13 20:44:27 +05:00
this . player . emit ( "debug" , this , ` Queue initialized: \ n \ n ${ this . player . scanDeps ( ) } ` ) ;
2021-06-11 15:32:22 +05:00
}
2021-06-14 22:51:54 +05:00
/ * *
* Returns current track
2021-06-20 19:22:09 +05:00
* @type { Track }
2021-06-14 22:51:54 +05:00
* /
2021-06-11 16:50:43 +05:00
get current() {
2021-08-07 19:25:21 +05:00
if ( this . # watchDestroyed ( ) ) return ;
2021-06-11 23:19:52 +05:00
return this . connection . audioResource ? . metadata ? ? this . tracks [ 0 ] ;
2021-06-11 16:50:43 +05:00
}
2021-06-24 10:24:34 +05:00
/ * *
* If this queue is destroyed
* @type { boolean }
* /
get destroyed() {
return this . # destroyed ;
}
2021-06-14 22:51:54 +05:00
/ * *
* Returns current track
* @returns { Track }
* /
2021-06-13 21:47:04 +05:00
nowPlaying() {
2021-08-07 19:25:21 +05:00
if ( this . # watchDestroyed ( ) ) return ;
2021-06-13 21:47:04 +05:00
return this . current ;
}
2021-06-14 22:51:54 +05:00
/ * *
* Connects to a voice channel
2021-07-04 18:12:27 +05:00
* @param { GuildChannelResolvable } channel The voice / stage channel
2021-06-14 22:51:54 +05:00
* @returns { Promise < Queue > }
* /
2021-07-04 18:12:27 +05:00
async connect ( channel : GuildChannelResolvable ) {
2021-08-07 19:25:21 +05:00
if ( this . # watchDestroyed ( ) ) return ;
2021-07-04 18:12:27 +05:00
const _channel = this . guild . channels . resolve ( channel ) as StageChannel | VoiceChannel ;
2021-08-07 19:25:21 +05:00
if ( ! [ "GUILD_STAGE_VOICE" , "GUILD_VOICE" ] . includes ( _channel ? . type ) )
throw new PlayerError ( ` Channel type must be GUILD_VOICE or GUILD_STAGE_VOICE, got ${ _channel ? . type } ! ` , ErrorStatusCode . INVALID_ARG_TYPE ) ;
2021-07-04 18:12:27 +05:00
const connection = await this . player . voiceUtils . connect ( _channel , {
2021-08-07 20:34:31 +05:00
deaf : this.options.autoSelfDeaf ,
maxTime : this.player.options.connectionTimeout || 20000
2021-06-14 19:44:15 +05:00
} ) ;
2021-06-11 23:19:52 +05:00
this . connection = connection ;
2021-06-11 15:32:22 +05:00
2021-07-10 15:01:45 +05:00
if ( _channel . type === "GUILD_STAGE_VOICE" ) {
2021-07-04 18:12:27 +05:00
await _channel . guild . me . voice . setSuppressed ( false ) . catch ( async ( ) = > {
return await _channel . guild . me . voice . setRequestToSpeak ( true ) . catch ( Util . noop ) ;
2021-06-23 15:34:53 +05:00
} ) ;
2021-06-23 14:45:11 +05:00
}
2021-06-12 11:37:41 +05:00
2021-08-11 11:31:59 +05:00
this . connection . on ( "error" , ( err ) = > {
if ( this . # watchDestroyed ( false ) ) return ;
this . player . emit ( "connectionError" , this , err ) ;
} ) ;
this . connection . on ( "debug" , ( msg ) = > {
if ( this . # watchDestroyed ( false ) ) return ;
this . player . emit ( "debug" , this , msg ) ;
} ) ;
2021-06-13 14:22:45 +05:00
2021-06-14 00:34:59 +05:00
this . player . emit ( "connectionCreate" , this , this . connection ) ;
2021-06-14 11:45:03 +05:00
2021-06-22 15:50:31 +05:00
this . connection . on ( "start" , ( resource ) = > {
2021-08-11 11:31:59 +05:00
if ( this . # watchDestroyed ( false ) ) return ;
2021-06-22 10:33:20 +05:00
this . playing = true ;
2021-06-23 15:34:53 +05:00
if ( ! this . _filtersUpdate && resource ? . metadata ) this . player . emit ( "trackStart" , this , resource ? . metadata ? ? this . current ) ;
2021-06-22 10:33:20 +05:00
this . _filtersUpdate = false ;
} ) ;
2021-06-22 15:50:31 +05:00
this . connection . on ( "finish" , async ( resource ) = > {
2021-08-11 11:31:59 +05:00
if ( this . # watchDestroyed ( false ) ) return ;
2021-06-22 10:33:20 +05:00
this . playing = false ;
if ( this . _filtersUpdate ) return ;
this . _streamTime = 0 ;
2021-06-23 15:34:53 +05:00
if ( resource && resource . metadata ) this . previousTracks . push ( resource . metadata ) ;
2021-06-22 10:33:20 +05:00
2021-07-26 08:44:21 +05:00
this . player . emit ( "trackEnd" , this , resource . metadata ) ;
2021-06-22 10:33:20 +05:00
if ( ! this . tracks . length && this . repeatMode === QueueRepeatMode . OFF ) {
if ( this . options . leaveOnEnd ) this . destroy ( ) ;
this . player . emit ( "queueEnd" , this ) ;
} else {
if ( this . repeatMode !== QueueRepeatMode . AUTOPLAY ) {
if ( this . repeatMode === QueueRepeatMode . TRACK ) return void this . play ( Util . last ( this . previousTracks ) , { immediate : true } ) ;
if ( this . repeatMode === QueueRepeatMode . QUEUE ) this . tracks . push ( Util . last ( this . previousTracks ) ) ;
const nextTrack = this . tracks . shift ( ) ;
this . play ( nextTrack , { immediate : true } ) ;
return ;
} else {
this . _handleAutoplay ( Util . last ( this . previousTracks ) ) ;
}
}
} ) ;
2021-06-11 15:32:22 +05:00
return this ;
}
2021-06-14 22:51:54 +05:00
/ * *
* Destroys this queue
2021-06-20 19:22:09 +05:00
* @param { boolean } [ disconnect = this . options . leaveOnStop ] If it should leave on destroy
* @returns { void }
2021-06-14 22:51:54 +05:00
* /
2021-06-18 00:09:02 +05:00
destroy ( disconnect = this . options . leaveOnStop ) {
2021-08-07 19:25:21 +05:00
if ( this . # watchDestroyed ( ) ) return ;
2021-07-04 13:09:32 +05:00
if ( this . connection ) this . connection . end ( ) ;
2021-07-04 19:27:56 +05:00
if ( disconnect ) this . connection ? . disconnect ( ) ;
2021-06-11 15:32:22 +05:00
this . player . queues . delete ( this . guild . id ) ;
2021-06-22 10:33:20 +05:00
this . player . voiceUtils . cache . delete ( this . guild . id ) ;
2021-06-24 10:24:34 +05:00
this . # destroyed = true ;
2021-06-11 15:32:22 +05:00
}
2021-06-11 16:50:43 +05:00
2021-06-14 22:51:54 +05:00
/ * *
* Skips current track
* @returns { boolean }
* /
2021-06-11 23:19:52 +05:00
skip() {
2021-08-07 19:25:21 +05:00
if ( this . # watchDestroyed ( ) ) return ;
2021-06-11 23:19:52 +05:00
if ( ! this . connection ) return false ;
2021-06-22 10:33:20 +05:00
this . _filtersUpdate = false ;
2021-06-12 00:18:53 +05:00
this . connection . end ( ) ;
return true ;
2021-06-11 23:19:52 +05:00
}
2021-06-14 22:51:54 +05:00
/ * *
* Adds single track to the queue
* @param { Track } track The track to add
* @returns { void }
* /
2021-06-11 23:19:52 +05:00
addTrack ( track : Track ) {
2021-08-07 19:25:21 +05:00
if ( this . # watchDestroyed ( ) ) return ;
if ( ! ( track instanceof Track ) ) throw new PlayerError ( "invalid track" , ErrorStatusCode . INVALID_TRACK ) ;
2021-06-13 13:17:50 +05:00
this . tracks . push ( track ) ;
2021-06-13 13:06:19 +05:00
this . player . emit ( "trackAdd" , this , track ) ;
2021-06-11 23:19:52 +05:00
}
2021-06-14 22:51:54 +05:00
/ * *
* Adds multiple tracks to the queue
* @param { Track [ ] } tracks Array of tracks to add
* /
2021-06-11 23:19:52 +05:00
addTracks ( tracks : Track [ ] ) {
2021-08-07 19:25:21 +05:00
if ( this . # watchDestroyed ( ) ) return ;
if ( ! tracks . every ( ( y ) = > y instanceof Track ) ) throw new PlayerError ( "invalid track" , ErrorStatusCode . INVALID_TRACK ) ;
2021-06-11 23:19:52 +05:00
this . tracks . push ( . . . tracks ) ;
2021-06-13 13:17:50 +05:00
this . player . emit ( "tracksAdd" , this , tracks ) ;
2021-06-11 23:19:52 +05:00
}
2021-06-14 22:51:54 +05:00
/ * *
* Sets paused state
* @param { boolean } paused The paused state
* @returns { boolean }
* /
2021-06-12 00:18:53 +05:00
setPaused ( paused? : boolean ) {
2021-08-07 19:25:21 +05:00
if ( this . # watchDestroyed ( ) ) return ;
2021-06-12 00:18:53 +05:00
if ( ! this . connection ) return false ;
2021-06-19 19:12:13 +05:00
return paused ? this . connection . pause ( true ) : this . connection . resume ( ) ;
2021-06-12 00:18:53 +05:00
}
2021-06-14 22:51:54 +05:00
/ * *
* Sets bitrate
2021-06-21 10:33:55 +05:00
* @param { number | auto } bitrate bitrate to set
2021-06-20 19:22:09 +05:00
* @returns { void }
2021-06-14 22:51:54 +05:00
* /
2021-06-12 11:37:41 +05:00
setBitrate ( bitrate : number | "auto" ) {
2021-08-07 19:25:21 +05:00
if ( this . # watchDestroyed ( ) ) return ;
2021-06-12 11:37:41 +05:00
if ( ! this . connection ? . audioResource ? . encoder ) return ;
if ( bitrate === "auto" ) bitrate = this . connection . channel ? . bitrate ? ? 64000 ;
this . connection . audioResource . encoder . setBitrate ( bitrate ) ;
}
2021-06-14 22:51:54 +05:00
/ * *
* Sets volume
* @param { number } amount The volume amount
* @returns { boolean }
* /
2021-06-13 13:06:19 +05:00
setVolume ( amount : number ) {
2021-08-07 19:25:21 +05:00
if ( this . # watchDestroyed ( ) ) return ;
2021-06-13 13:06:19 +05:00
if ( ! this . connection ) return false ;
2021-06-26 11:40:24 +05:00
this . # lastVolume = amount ;
2021-06-13 14:06:01 +05:00
this . options . initialVolume = amount ;
2021-06-13 13:06:19 +05:00
return this . connection . setVolume ( amount ) ;
}
2021-06-14 22:51:54 +05:00
/ * *
* Sets repeat mode
* @param { QueueRepeatMode } mode The repeat mode
* @returns { boolean }
* /
2021-06-13 17:43:36 +05:00
setRepeatMode ( mode : QueueRepeatMode ) {
2021-08-07 19:25:21 +05:00
if ( this . # watchDestroyed ( ) ) return ;
if ( ! [ QueueRepeatMode . OFF , QueueRepeatMode . QUEUE , QueueRepeatMode . TRACK , QueueRepeatMode . AUTOPLAY ] . includes ( mode ) )
throw new PlayerError ( ` Unknown repeat mode " ${ mode } "! ` , ErrorStatusCode . UNKNOWN_REPEAT_MODE ) ;
2021-06-19 19:12:13 +05:00
if ( mode === this . repeatMode ) return false ;
2021-06-13 17:43:36 +05:00
this . repeatMode = mode ;
return true ;
}
2021-06-14 22:51:54 +05:00
/ * *
2021-06-20 19:22:09 +05:00
* The current volume amount
* @type { number }
2021-06-14 22:51:54 +05:00
* /
2021-06-13 13:06:19 +05:00
get volume() {
2021-08-07 19:25:21 +05:00
if ( this . # watchDestroyed ( ) ) return ;
2021-06-13 13:06:19 +05:00
if ( ! this . connection ) return 100 ;
return this . connection . volume ;
}
2021-06-17 18:22:03 +05:00
set volume ( amount : number ) {
this . setVolume ( amount ) ;
}
2021-06-20 19:22:09 +05:00
/ * *
* The stream time of this queue
* @type { number }
* /
2021-06-19 23:10:38 +05:00
get streamTime() {
2021-08-07 19:25:21 +05:00
if ( this . # watchDestroyed ( ) ) return ;
2021-06-19 23:10:38 +05:00
if ( ! this . connection ) return 0 ;
2021-06-20 10:37:59 +05:00
const playbackTime = this . _streamTime + this . connection . streamTime ;
const NC = this . _activeFilters . includes ( "nightcore" ) ? 1.25 : null ;
const VW = this . _activeFilters . includes ( "vaporwave" ) ? 0.8 : null ;
if ( NC && VW ) return playbackTime * ( NC + VW ) ;
return NC ? playbackTime * NC : VW ? playbackTime * VW : playbackTime ;
}
2021-06-24 10:20:12 +05:00
set streamTime ( time : number ) {
2021-08-07 19:25:21 +05:00
if ( this . # watchDestroyed ( ) ) return ;
2021-06-24 10:20:12 +05:00
this . seek ( time ) ;
}
2021-06-20 19:22:09 +05:00
/ * *
* Returns enabled filters
* @returns { AudioFilters }
* /
2021-06-20 10:37:59 +05:00
getFiltersEnabled() {
2021-08-07 19:25:21 +05:00
if ( this . # watchDestroyed ( ) ) return ;
2021-06-20 10:37:59 +05:00
return AudioFilters . names . filter ( ( x ) = > this . _activeFilters . includes ( x ) ) ;
2021-06-19 23:10:38 +05:00
}
2021-06-20 19:22:09 +05:00
/ * *
* Returns disabled filters
* @returns { AudioFilters }
* /
2021-06-20 10:37:59 +05:00
getFiltersDisabled() {
2021-08-07 19:25:21 +05:00
if ( this . # watchDestroyed ( ) ) return ;
2021-06-20 10:37:59 +05:00
return AudioFilters . names . filter ( ( x ) = > ! this . _activeFilters . includes ( x ) ) ;
}
2021-06-20 19:22:09 +05:00
/ * *
* Sets filters
* @param { QueueFilters } filters Queue filters
* @returns { Promise < void > }
* /
2021-06-20 10:37:59 +05:00
async setFilters ( filters? : QueueFilters ) {
2021-08-07 19:25:21 +05:00
if ( this . # watchDestroyed ( ) ) return ;
2021-06-20 10:37:59 +05:00
if ( ! filters || ! Object . keys ( filters ) . length ) {
// reset filters
const streamTime = this . streamTime ;
this . _activeFilters = [ ] ;
return await this . play ( this . current , {
immediate : true ,
filtersUpdate : true ,
seek : streamTime ,
encoderArgs : [ ]
} ) ;
}
2021-06-19 23:10:38 +05:00
2021-06-22 15:24:05 +05:00
const _filters : any [ ] = [ ] ; // eslint-disable-line @typescript-eslint/no-explicit-any
2021-06-19 23:10:38 +05:00
for ( const filter in filters ) {
if ( filters [ filter as keyof QueueFilters ] === true ) _filters . push ( filter ) ;
}
2021-06-20 10:37:59 +05:00
if ( this . _activeFilters . join ( "" ) === _filters . join ( "" ) ) return ;
2021-06-20 14:30:39 +05:00
const newFilters = AudioFilters . create ( _filters ) . trim ( ) ;
2021-06-20 10:37:59 +05:00
const streamTime = this . streamTime ;
this . _activeFilters = _filters ;
2021-06-19 23:10:38 +05:00
return await this . play ( this . current , {
2021-06-20 00:48:48 +05:00
immediate : true ,
2021-06-19 23:10:38 +05:00
filtersUpdate : true ,
2021-06-20 10:37:59 +05:00
seek : streamTime ,
2021-06-20 14:30:39 +05:00
encoderArgs : ! _filters . length ? undefined : [ "-af" , newFilters ]
2021-06-19 23:10:38 +05:00
} ) ;
}
2021-06-20 19:22:09 +05:00
/ * *
* Seeks to the given time
* @param { number } position The position
* @returns { boolean }
* /
2021-06-20 11:57:17 +05:00
async seek ( position : number ) {
2021-08-07 19:25:21 +05:00
if ( this . # watchDestroyed ( ) ) return ;
2021-06-20 11:57:17 +05:00
if ( ! this . playing || ! this . current ) return false ;
if ( position < 1 ) position = 0 ;
if ( position >= this . current . durationMS ) return this . skip ( ) ;
2021-06-20 12:36:16 +05:00
2021-06-20 11:57:17 +05:00
await this . play ( this . current , {
immediate : true ,
filtersUpdate : true , // to stop events
seek : position
} ) ;
return true ;
}
2021-06-14 22:51:54 +05:00
/ * *
* Plays previous track
* @returns { Promise < void > }
* /
2021-06-13 17:46:15 +05:00
async back() {
2021-08-07 19:25:21 +05:00
if ( this . # watchDestroyed ( ) ) return ;
2021-06-22 10:33:20 +05:00
const prev = this . previousTracks [ this . previousTracks . length - 2 ] ; // because last item is the current track
2021-08-07 19:25:21 +05:00
if ( ! prev ) throw new PlayerError ( "Could not find previous track" , ErrorStatusCode . TRACK_NOT_FOUND ) ;
2021-06-22 10:33:20 +05:00
return await this . play ( prev , { immediate : true } ) ;
}
2021-06-22 19:38:00 +05:00
/ * *
* Clear this queue
* /
2021-06-22 10:33:20 +05:00
clear() {
2021-08-07 19:25:21 +05:00
if ( this . # watchDestroyed ( ) ) return ;
2021-06-22 10:33:20 +05:00
this . tracks = [ ] ;
this . previousTracks = [ ] ;
2021-06-13 17:46:15 +05:00
}
2021-06-24 10:20:12 +05:00
/ * *
* Stops the player
* @returns { void }
* /
stop() {
2021-08-07 19:25:21 +05:00
if ( this . # watchDestroyed ( ) ) return ;
2021-06-24 10:20:12 +05:00
return this . destroy ( ) ;
}
/ * *
* Shuffles this queue
* @returns { boolean }
* /
shuffle() {
2021-08-07 19:25:21 +05:00
if ( this . # watchDestroyed ( ) ) return ;
2021-06-24 10:20:12 +05:00
if ( ! this . tracks . length || this . tracks . length < 3 ) return false ;
const currentTrack = this . tracks . shift ( ) ;
for ( let i = this . tracks . length - 1 ; i > 0 ; i -- ) {
const j = Math . floor ( Math . random ( ) * ( i + 1 ) ) ;
[ this . tracks [ i ] , this . tracks [ j ] ] = [ this . tracks [ j ] , this . tracks [ i ] ] ;
}
this . tracks . unshift ( currentTrack ) ;
return true ;
}
/ * *
* Removes a track from the queue
2021-06-26 00:14:07 +05:00
* @param { Track | Snowflake | number } track The track to remove
2021-06-24 10:20:12 +05:00
* @returns { Track }
* /
2021-06-26 00:14:07 +05:00
remove ( track : Track | Snowflake | number ) {
2021-08-07 19:25:21 +05:00
if ( this . # watchDestroyed ( ) ) return ;
2021-06-24 10:20:12 +05:00
let trackFound : Track = null ;
if ( typeof track === "number" ) {
trackFound = this . tracks [ track ] ;
if ( trackFound ) {
2021-06-26 00:14:07 +05:00
this . tracks = this . tracks . filter ( ( t ) = > t . id !== trackFound . id ) ;
2021-06-24 10:20:12 +05:00
}
} else {
2021-06-26 00:14:07 +05:00
trackFound = this . tracks . find ( ( s ) = > s . id === ( track instanceof Track ? track.id : track ) ) ;
2021-06-24 10:20:12 +05:00
if ( trackFound ) {
2021-06-26 00:14:07 +05:00
this . tracks = this . tracks . filter ( ( s ) = > s . id !== trackFound . id ) ;
2021-06-24 10:20:12 +05:00
}
}
return trackFound ;
}
/ * *
* Jumps to particular track
* @param { Track | number } track The track
* @returns { void }
* /
jump ( track : Track | number ) : void {
2021-08-07 19:25:21 +05:00
if ( this . # watchDestroyed ( ) ) return ;
2021-08-23 17:31:53 +05:00
// remove the track if exists
2021-06-24 10:20:12 +05:00
const foundTrack = this . remove ( track ) ;
2021-08-07 19:25:21 +05:00
if ( ! foundTrack ) throw new PlayerError ( "Track not found" , ErrorStatusCode . TRACK_NOT_FOUND ) ;
2021-08-23 17:31:53 +05:00
// since we removed the existing track from the queue,
// we now have to place that to position 1
// because we want to jump to that track
// this will skip current track and play the next one which will be the foundTrack
this . tracks . splice ( 1 , 0 , foundTrack ) ;
2021-06-24 10:20:12 +05:00
return void this . skip ( ) ;
}
2021-06-30 00:29:44 +05:00
/ * *
* Inserts the given track to specified index
* @param { Track } track The track to insert
* @param { number } [ index = 0 ] The index where this track should be
* /
insert ( track : Track , index = 0 ) {
2021-08-07 19:25:21 +05:00
if ( ! track || ! ( track instanceof Track ) ) throw new PlayerError ( "track must be the instance of Track" , ErrorStatusCode . INVALID_TRACK ) ;
if ( typeof index !== "number" || index < 0 || ! Number . isFinite ( index ) ) throw new PlayerError ( ` Invalid index " ${ index } " ` , ErrorStatusCode . INVALID_ARG_TYPE ) ;
2021-06-30 00:29:44 +05:00
this . tracks . splice ( index , 0 , track ) ;
this . player . emit ( "trackAdd" , this , track ) ;
}
2021-06-24 10:20:12 +05:00
/ * *
* @typedef { object } PlayerTimestamp
2021-06-24 10:24:34 +05:00
* @property { string } current The current progress
* @property { string } end The total time
* @property { number } progress Progress in %
2021-06-24 10:20:12 +05:00
* /
/ * *
* Returns player stream timestamp
* @returns { PlayerTimestamp }
* /
2021-07-05 11:28:06 +05:00
getPlayerTimestamp() {
2021-08-07 19:25:21 +05:00
if ( this . # watchDestroyed ( ) ) return ;
2021-07-05 11:28:06 +05:00
const currentStreamTime = this . streamTime ;
const totalTime = this . current . durationMS ;
2021-06-24 10:20:12 +05:00
const currentTimecode = Util . buildTimeCode ( Util . parseMS ( currentStreamTime ) ) ;
const endTimecode = Util . buildTimeCode ( Util . parseMS ( totalTime ) ) ;
return {
current : currentTimecode ,
end : endTimecode ,
progress : Math.round ( ( currentStreamTime / totalTime ) * 100 )
} ;
}
/ * *
* Creates progress bar string
* @param { PlayerProgressbarOptions } options The progress bar options
* @returns { string }
* /
2021-07-05 11:28:06 +05:00
createProgressBar ( options : PlayerProgressbarOptions = { timecodes : true } ) {
2021-08-07 19:25:21 +05:00
if ( this . # watchDestroyed ( ) ) return ;
2021-06-24 10:20:12 +05:00
const length = typeof options . length === "number" ? ( options . length <= 0 || options . length === Infinity ? 15 : options.length ) : 15 ;
2021-07-05 11:28:06 +05:00
const index = Math . round ( ( this . streamTime / this . current . durationMS ) * length ) ;
2021-06-24 10:20:12 +05:00
const indicator = typeof options . indicator === "string" && options . indicator . length > 0 ? options . indicator : "🔘" ;
const line = typeof options . line === "string" && options . line . length > 0 ? options . line : "▬" ;
if ( index >= 1 && index <= length ) {
const bar = line . repeat ( length - 1 ) . split ( "" ) ;
bar . splice ( index , 0 , indicator ) ;
if ( options . timecodes ) {
2021-07-05 11:28:06 +05:00
const timestamp = this . getPlayerTimestamp ( ) ;
2021-06-24 10:20:12 +05:00
return ` ${ timestamp . current } ┃ ${ bar . join ( "" ) } ┃ ${ timestamp . end } ` ;
} else {
return ` ${ bar . join ( "" ) } ` ;
}
} else {
if ( options . timecodes ) {
2021-07-05 11:28:06 +05:00
const timestamp = this . getPlayerTimestamp ( ) ;
2021-06-24 10:20:12 +05:00
return ` ${ timestamp . current } ┃ ${ indicator } ${ line . repeat ( length - 1 ) } ┃ ${ timestamp . end } ` ;
} else {
return ` ${ indicator } ${ line . repeat ( length - 1 ) } ` ;
}
}
}
/ * *
* Total duration
* @type { Number }
* /
get totalTime ( ) : number {
2021-08-07 19:25:21 +05:00
if ( this . # watchDestroyed ( ) ) return ;
2021-06-24 10:20:12 +05:00
return this . tracks . length > 0 ? this . tracks . map ( ( t ) = > t . durationMS ) . reduce ( ( p , c ) = > p + c ) : 0 ;
}
2021-06-14 22:51:54 +05:00
/ * *
2021-06-20 20:00:19 +05:00
* Play stream in a voice / stage channel
2021-06-20 19:22:09 +05:00
* @param { Track } [ src ] The track to play ( if empty , uses first track from the queue )
* @param { PlayOptions } [ options = { } ] The options
2021-06-14 22:51:54 +05:00
* @returns { Promise < void > }
* /
2021-06-13 18:09:25 +05:00
async play ( src? : Track , options : PlayOptions = { } ) : Promise < void > {
2021-08-11 11:31:59 +05:00
if ( this . # watchDestroyed ( false ) ) return ;
2021-08-07 19:25:21 +05:00
if ( ! this . connection || ! this . connection . voiceConnection ) throw new PlayerError ( "Voice connection is not available, use <Queue>.connect()!" , ErrorStatusCode . NO_CONNECTION ) ;
2021-06-13 13:06:19 +05:00
if ( src && ( this . playing || this . tracks . length ) && ! options . immediate ) return this . addTrack ( src ) ;
2021-06-19 23:10:38 +05:00
const track = options . filtersUpdate && ! options . immediate ? src || this . current : src ? ? this . tracks . shift ( ) ;
2021-06-11 23:19:52 +05:00
if ( ! track ) return ;
2021-06-13 21:47:04 +05:00
2021-07-03 18:46:39 +05:00
this . player . emit ( "debug" , this , "Received play request" ) ;
2021-06-13 21:47:04 +05:00
if ( ! options . filtersUpdate ) {
2021-06-26 00:14:07 +05:00
this . previousTracks = this . previousTracks . filter ( ( x ) = > x . id !== track . id ) ;
2021-06-23 14:45:11 +05:00
this . previousTracks . push ( track ) ;
2021-06-13 21:47:04 +05:00
}
2021-08-13 20:44:27 +05:00
// TODO: remove discord-ytdl-core
2021-06-22 10:33:20 +05:00
let stream ;
2021-06-11 23:19:52 +05:00
if ( [ "youtube" , "spotify" ] . includes ( track . raw . source ) ) {
2021-06-13 18:09:25 +05:00
if ( track . raw . source === "spotify" && ! track . raw . engine ) {
track . raw . engine = await YouTube . search ( ` ${ track . author } ${ track . title } ` , { type : "video" } )
. then ( ( x ) = > x [ 0 ] . url )
. catch ( ( ) = > null ) ;
}
const link = track . raw . source === "spotify" ? track.raw.engine : track.url ;
if ( ! link ) return void this . play ( this . tracks . shift ( ) , { immediate : true } ) ;
stream = ytdl ( link , {
2021-06-14 19:44:15 +05:00
. . . this . options . ytdlOptions ,
// discord-ytdl-core
2021-06-11 23:19:52 +05:00
opusEncoded : false ,
2021-06-12 11:37:41 +05:00
fmt : "s16le" ,
2021-06-20 10:37:59 +05:00
encoderArgs : options.encoderArgs ? ? this . _activeFilters . length ? [ "-af" , AudioFilters . create ( this . _activeFilters ) ] : [ ] ,
seek : options.seek ? options . seek / 1000 : 0
2021-06-20 15:06:52 +05:00
} ) . on ( "error" , ( err ) = > {
return err . message . toLowerCase ( ) . includes ( "premature close" ) ? null : this . player . emit ( "error" , this , err ) ;
} ) ;
2021-06-11 23:19:52 +05:00
} else {
2021-06-18 00:09:02 +05:00
stream = ytdl
. arbitraryStream (
track . raw . source === "soundcloud" ? await track . raw . engine . downloadProgressive ( ) : typeof track . raw . engine === "function" ? await track . raw . engine ( ) : track . raw . engine ,
{
opusEncoded : false ,
fmt : "s16le" ,
2021-06-20 10:37:59 +05:00
encoderArgs : options.encoderArgs ? ? this . _activeFilters . length ? [ "-af" , AudioFilters . create ( this . _activeFilters ) ] : [ ] ,
seek : options.seek ? options . seek / 1000 : 0
2021-06-18 00:09:02 +05:00
}
)
2021-06-20 15:06:52 +05:00
. on ( "error" , ( err ) = > {
return err . message . toLowerCase ( ) . includes ( "premature close" ) ? null : this . player . emit ( "error" , this , err ) ;
} ) ;
2021-06-11 23:19:52 +05:00
}
2021-06-12 18:22:45 +05:00
const resource : AudioResource < Track > = this . connection . createStream ( stream , {
type : StreamType . Raw ,
data : track
} ) ;
2021-06-19 23:10:38 +05:00
if ( options . seek ) this . _streamTime = options . seek ;
2021-06-22 10:33:20 +05:00
this . _filtersUpdate = options . filtersUpdate ;
2021-08-23 17:25:31 +05:00
this . setVolume ( this . options . initialVolume ) ;
2021-06-19 23:10:38 +05:00
2021-07-21 10:27:40 +05:00
setTimeout ( ( ) = > {
2021-08-23 17:25:31 +05:00
this . connection . playStream ( resource ) ;
2021-08-07 23:35:51 +05:00
} , this . # getBufferingTimeout ( ) ) . unref ( ) ;
2021-06-22 10:33:20 +05:00
}
2021-06-11 23:19:52 +05:00
2021-06-22 19:38:00 +05:00
/ * *
* Private method to handle autoplay
* @param { Track } track The source track to find its similar track for autoplay
* @returns { Promise < void > }
2021-06-25 12:24:53 +05:00
* @private
2021-06-22 19:38:00 +05:00
* /
2021-06-22 10:33:20 +05:00
private async _handleAutoplay ( track : Track ) : Promise < void > {
2021-08-07 19:25:21 +05:00
if ( this . # watchDestroyed ( ) ) return ;
2021-06-22 10:33:20 +05:00
if ( ! track || ! [ track . source , track . raw ? . source ] . includes ( "youtube" ) ) {
if ( this . options . leaveOnEnd ) this . destroy ( ) ;
return void this . player . emit ( "queueEnd" , this ) ;
}
2021-06-23 14:45:11 +05:00
const info = await YouTube . getVideo ( track . url )
. then ( ( x ) = > x . videos [ 0 ] )
. catch ( Util . noop ) ;
2021-06-22 10:33:20 +05:00
if ( ! info ) {
if ( this . options . leaveOnEnd ) this . destroy ( ) ;
return void this . player . emit ( "queueEnd" , this ) ;
}
2021-06-20 12:36:05 +05:00
2021-06-22 10:33:20 +05:00
const nextTrack = new Track ( this . player , {
title : info.title ,
url : ` https://www.youtube.com/watch?v= ${ info . id } ` ,
2021-06-23 14:45:11 +05:00
duration : info.durationFormatted ? Util . buildTimeCode ( Util . parseMS ( info . duration * 1000 ) ) : "0:00" ,
2021-06-22 10:33:20 +05:00
description : "" ,
2021-06-23 14:45:11 +05:00
thumbnail : typeof info . thumbnail === "string" ? info.thumbnail : info.thumbnail.url ,
views : info.views ,
author : info.channel.name ,
2021-06-22 10:33:20 +05:00
requestedBy : track.requestedBy ,
source : "youtube"
2021-06-11 23:19:52 +05:00
} ) ;
2021-06-22 10:33:20 +05:00
this . play ( nextTrack , { immediate : true } ) ;
2021-06-11 16:50:43 +05:00
}
2021-06-11 19:57:49 +05:00
* [ Symbol . iterator ] ( ) {
2021-08-07 19:25:21 +05:00
if ( this . # watchDestroyed ( ) ) return ;
2021-06-11 19:57:49 +05:00
yield * this . tracks ;
}
2021-06-11 23:19:52 +05:00
2021-06-14 22:51:54 +05:00
/ * *
* JSON representation of this queue
* @returns { object }
* /
2021-06-11 23:19:52 +05:00
toJSON() {
2021-08-07 19:25:21 +05:00
if ( this . # watchDestroyed ( ) ) return ;
2021-06-11 23:19:52 +05:00
return {
2021-06-26 00:14:07 +05:00
id : this.id ,
2021-06-11 23:19:52 +05:00
guild : this.guild.id ,
2021-06-20 12:36:05 +05:00
voiceChannel : this.connection?.channel?.id ,
2021-06-11 23:19:52 +05:00
options : this.options ,
tracks : this.tracks.map ( ( m ) = > m . toJSON ( ) )
} ;
}
2021-06-14 22:51:54 +05:00
/ * *
* String representation of this queue
* @returns { string }
* /
2021-06-11 23:19:52 +05:00
toString() {
2021-08-07 19:25:21 +05:00
if ( this . # watchDestroyed ( ) ) return ;
2021-06-11 23:19:52 +05:00
if ( ! this . tracks . length ) return "No songs available to display!" ;
return ` **Upcoming Songs:** \ n ${ this . tracks . map ( ( m , i ) = > ` ${ i + 1 } . ** ${ m . title } ** ` ) . join ( "\n" ) } ` ;
}
2021-06-22 10:33:20 +05:00
2021-08-11 11:31:59 +05:00
# watchDestroyed ( emit = true ) {
2021-08-07 19:25:21 +05:00
if ( this . # destroyed ) {
2021-08-11 11:31:59 +05:00
if ( emit ) this . player . emit ( "error" , this , new PlayerError ( "Cannot use destroyed queue" , ErrorStatusCode . DESTROYED_QUEUE ) ) ;
2021-08-07 19:25:21 +05:00
return true ;
}
2021-08-11 11:31:59 +05:00
return false ;
2021-06-22 10:33:20 +05:00
}
2021-07-21 10:27:40 +05:00
# getBufferingTimeout() {
const timeout = this . options . bufferingTimeout ;
if ( isNaN ( timeout ) || timeout < 0 || ! Number . isFinite ( timeout ) ) return 1000 ;
return timeout ;
}
2021-06-11 15:32:22 +05:00
}
2021-06-13 23:28:37 +05:00
export { Queue } ;