diff --git a/index.ts b/index.ts index 95e7041..2d5d5dc 100644 --- a/index.ts +++ b/index.ts @@ -1,6 +1,6 @@ import Filters from './lib/Filters'; import Node, { NodeState } from './lib/Node'; -import Player, { ConnectionState } from './lib/Player'; +import { Player, ConnectionState } from './lib/Player'; import Track from './lib/Track'; import UnresolvedTrack from './lib/UnresolvedTrack'; import { Vulkava } from './lib/Vulkava'; @@ -39,4 +39,4 @@ export { NodeOptions, VERSION -}; \ No newline at end of file +}; diff --git a/lib/@types/index.ts b/lib/@types/index.ts index e3330e9..af5cfae 100644 --- a/lib/@types/index.ts +++ b/lib/@types/index.ts @@ -1,5 +1,5 @@ -import { Node } from '../..'; -import Player from '../Player'; +import { DefaultQueue, Node } from '../..'; +import { Player } from '../Player'; import { AbstractQueue } from '../queue/AbstractQueue'; import Track from '../Track'; import UnresolvedTrack from '../UnresolvedTrack'; @@ -74,74 +74,101 @@ export type VulkavaOptions = { /** Vulkava events */ export interface VulkavaEvents { debug: [message: string]; - raw: [ + error: [ node: Node, - payload: unknown, + error: Error, ]; - nodeConnect: [node: Node]; - nodeResume: [node: Node]; - nodeDisconnect: [ - node: Node, + playerCreate: [player: Player]; + playerDestroy: [player: Player]; + playerDisconnect: [ + player: Player, code: number, reason: string, ]; - warn: [ + playerUpdate: [ + oldPlayer: Player, + newPlayer: Player, + ]; + pong: [ node: Node, - warn: string, + ping?: number, ]; - error: [ + queueEnd: [player: Player]; + raw: [ node: Node, - error: Error, + payload: unknown, ]; - trackStart: [ + recordFinished: [ + node: Node, + guildId: string, + id: string, + ]; + + // This event only works on my lavalink (https://github.com/davidffa/lavalink/releases) and while recording audio + speakingStart: [ player: Player, - track: Track, + userId: string, ]; + // This event only works on my lavalink (https://github.com/davidffa/lavalink/releases) and while recording audio + speakingStop: [ + player: Player, + userId: string, + ]; + trackEnd: [ player: Player, track: Track, reason: TrackEndReason, ]; + trackException: [ + player: Player, + track: Track | UnresolvedTrack, + exception: LoadException & { cause: string }, + ]; + trackStart: [ + player: Player, + track: Track, + ]; trackStuck: [ player: Player, track: Track, thresholdMs: number, ]; - trackException: [ + + // This event only works on my lavalink (https://github.com/davidffa/lavalink/releases) and while recording audio + userDisconnect: [ player: Player, - track: Track | UnresolvedTrack, - exception: LoadException & { cause: string }, + userId: string, ]; - playerCreate: [player: Player]; - playerDestroy: [player: Player]; - playerDisconnect: [ - player: Player, + + nodeConnect: [node: Node]; + nodeDisconnect: [ + node: Node, code: number, reason: string, ]; - queueEnd: [player: Player]; - pong: [ - node: Node, - ping?: number, - ]; - recordFinished: [ + nodeResume: [node: Node]; + warn: [ node: Node, - guildId: string, - id: string, + warn: string, ]; +} - // Speaking Events (only work on my lavalink (https://github.com/davidffa/lavalink/releases) and while recording audio) - speakingStart: [ +/** Player events */ +export interface PlayerEvents { + queueShuffle: [ player: Player, - userId: string, + oldQueue: DefaultQueue, + newQueue: DefaultQueue, ]; - speakingStop: [ + seek: [ player: Player, - userId: string, + oldPosition: number, + newPosition: number, ]; - userDisconnect: [ + skip: [ player: Player, - userId: string, + amount: number, ]; } diff --git a/lib/Filters.ts b/lib/Filters.ts index 6758ed5..0d3335e 100644 --- a/lib/Filters.ts +++ b/lib/Filters.ts @@ -9,7 +9,7 @@ import { TremoloOptions, VibratoOptions } from './@types'; -import Player from './Player'; +import { Player } from './Player'; export default class Filters { private readonly player: Player; @@ -269,4 +269,4 @@ export default class Filters { this.player.node?.send(payload); } -} \ No newline at end of file +} diff --git a/lib/Player.ts b/lib/Player.ts index 00c31c5..ff0cf3a 100644 --- a/lib/Player.ts +++ b/lib/Player.ts @@ -1,5 +1,9 @@ +/* eslint-disable @typescript-eslint/no-this-alias */ + +import { EventEmitter } from 'events'; + import { Node, Vulkava, AbstractQueue } from '..'; -import { PlayerOptions, PlayerState, PlayOptions, VoiceState } from './@types'; +import { PlayerEvents, PlayerOptions, PlayerState, PlayOptions, VoiceState } from './@types'; import Filters from './Filters'; import { NodeState } from './Node'; import { DefaultQueue } from './queue/DefaultQueue'; @@ -13,8 +17,15 @@ export enum ConnectionState { DISCONNECTED } +// eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging +export interface Player { + on(event: Event, listener: (...args: PlayerEvents[Event]) => void): this; + once(event: Event, listener: (...args: PlayerEvents[Event]) => void): this; +} + /** * Represents a Player structure + * @extends EventEmitter * @prop {Node} node - The node that this player is connected to * @prop {Filters} filters - The filters instance of this player * @prop {String} guildId - The guild id of this player @@ -31,7 +42,8 @@ export enum ConnectionState { * @prop {State} state - The state of this player (CONNECTING, CONNECTED, DISCONNECTED) * @prop {Object} voiceState - The player voicestate */ -export default class Player { +// eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging +export class Player extends EventEmitter { private readonly vulkava: Vulkava; public node: Node | null; @@ -88,6 +100,8 @@ export default class Player { * @param {AbstractQueue} [options.queue] - The queue for this player */ constructor(vulkava: Vulkava, options: PlayerOptions) { + super(); + Player.checkOptions(options); this.vulkava = vulkava; @@ -167,7 +181,11 @@ export default class Player { private assignNode() { const node = this.vulkava.bestNode; + const oldPlayer = this; + this.node = node; + + this.vulkava.emit('playerUpdate', oldPlayer, this); this.vulkava.emit('debug', `Assigned node ${node.identifier} to player ${this.guildId}`); } @@ -320,6 +338,8 @@ export default class Player { this.assignNode(); } + const oldPlayer = this; + if (!this.current) { let newTrack = await this.queue.poll(); @@ -342,6 +362,8 @@ export default class Player { this.playing = true; + this.vulkava.emit('playerUpdate', oldPlayer, this); + if (this.node?.options.transport === 'rest') { this.node?.rest.updatePlayer(this.guildId, { encodedTrack: this.current.encodedTrack, @@ -408,7 +430,13 @@ export default class Player { * @param {Boolean} state - Whether to enable track looping or not */ public setTrackLoop(state: boolean) { + const oldPlayer = this; + this.trackRepeat = state; + + if (oldPlayer.trackRepeat !== state) { + this.vulkava.emit('playerUpdate', oldPlayer, this); + } } /** @@ -416,7 +444,13 @@ export default class Player { * @param {Boolean} state - Whether to enable queue looping or not */ public setQueueLoop(state: boolean) { + const oldPlayer = this; + this.queueRepeat = state; + + if (oldPlayer.queueRepeat !== state) { + this.vulkava.emit('playerUpdate', oldPlayer, this); + } } /** @@ -427,9 +461,13 @@ export default class Player { if (!channelId || typeof channelId !== 'string') throw new TypeError('Voice channel id must be a string.'); if (this.voiceChannelId === channelId) return; + const oldPlayer = this; + this.voiceChannelId = channelId; this.state = ConnectionState.DISCONNECTED; this.connect(); + + this.vulkava.emit('playerUpdate', oldPlayer, this); } /** @@ -438,7 +476,11 @@ export default class Player { */ public shuffleQueue() { if (this.queue instanceof DefaultQueue) { + const oldQueue = this.queue as DefaultQueue; + (this.queue as DefaultQueue).shuffle(); + + this.emit('queueShuffle', this, oldQueue, this.queue); } } @@ -455,6 +497,8 @@ export default class Player { await this.queue.skipNTracks(amount); } + this.emit('skip', this, amount > this.queue.size ? this.queue.size : amount); + if (this.node?.options.transport === 'rest') { this.node.rest.updatePlayer(this.guildId, { encodedTrack: null @@ -481,8 +525,14 @@ export default class Player { if (this.node === null) throw new Error('Assertion failed. The player does not have a node.'); + const oldPlayer = this; + this.paused = state; + if (oldPlayer.paused !== state) { + this.vulkava.emit('playerUpdate', oldPlayer, this); + } + if (this.node?.options.transport === 'rest') { this.node.rest.updatePlayer(this.guildId, { paused: state @@ -512,6 +562,8 @@ export default class Player { return; } + this.emit('seek', this, this.current.position, position); + if (this.node?.options.transport === 'rest') { this.node.rest.updatePlayer(this.guildId, { position @@ -558,13 +610,31 @@ export default class Player { } public update(state: PlayerState): void { - if (state.position) this.position = state.position; - if (state.time) this.positionTimestamp = state.time; + const oldPlayer = this; + let hasChanged = false; + + if (state.position && this.position !== state.position) { + this.position = state.position; + hasChanged = true; + } + + if (state.time && this.positionTimestamp !== state.time) { + this.positionTimestamp = state.time; + hasChanged = true; + } if (state.connected) { - this.state = ConnectionState.CONNECTED; + if (this.state !== ConnectionState.CONNECTED) { + this.state = ConnectionState.CONNECTED; + hasChanged = true; + } } else if (this.state === ConnectionState.CONNECTED) { this.state = ConnectionState.DISCONNECTED; + hasChanged = true; + } + + if (hasChanged) { + this.vulkava.emit('playerUpdate', oldPlayer, this); } } } diff --git a/lib/Recorder.ts b/lib/Recorder.ts index d913360..6efe42f 100644 --- a/lib/Recorder.ts +++ b/lib/Recorder.ts @@ -1,5 +1,5 @@ import { RecordOptions } from './@types'; -import Player from './Player'; +import { Player } from './Player'; import { Vulkava } from './Vulkava'; export default class Recorder { @@ -76,4 +76,4 @@ export default class Recorder { guildId: this.player.guildId }); } -} \ No newline at end of file +}