diff --git a/src/commands/pause.ts b/src/commands/pause.ts new file mode 100644 index 0000000..54855e0 --- /dev/null +++ b/src/commands/pause.ts @@ -0,0 +1,29 @@ +import {Message} from 'discord.js'; +import {TYPES} from '../types'; +import {inject, injectable} from 'inversify'; +import PlayerManager from '../managers/player'; +import {STATUS} from '../services/player'; +import Command from '.'; + +@injectable() +export default class implements Command { + public name = 'pause'; + public description = 'pause currently playing song'; + private readonly playerManager: PlayerManager; + + constructor(@inject(TYPES.Managers.Player) playerManager: PlayerManager) { + this.playerManager = playerManager; + } + + public async execute(msg: Message, _: string []): Promise { + const player = this.playerManager.get(msg.guild!.id); + + if (player.status !== STATUS.PLAYING) { + await msg.channel.send('error: not currently playing'); + return; + } + + player.pause(); + await msg.channel.send('paused'); + } +} diff --git a/src/commands/play.ts b/src/commands/play.ts index 03992d7..2d91d19 100644 --- a/src/commands/play.ts +++ b/src/commands/play.ts @@ -36,6 +36,29 @@ export default class implements Command { } public async execute(msg: Message, args: string []): Promise { + const queue = this.queueManager.get(msg.guild!.id); + + if (args.length === 0) { + if (this.playerManager.get(msg.guild!.id).status === STATUS.PLAYING) { + await msg.channel.send('error: already playing, give me a song name'); + return; + } + + // Must be resuming play + if (queue.get().length === 0) { + await msg.channel.send('error: nothing to play'); + return; + } + + const channel = getMostPopularVoiceChannel(msg.guild!); + + await this.playerManager.get(msg.guild!.id).connect(channel); + await this.playerManager.get(msg.guild!.id).play(); + + await msg.channel.send('play resuming'); + return; + } + const newSongs: QueuedSong[] = []; const res = new LoadingMessage(msg.channel as TextChannel, 'hold on a sec'); diff --git a/src/inversify.config.ts b/src/inversify.config.ts index 759a27ac..afdd5b8 100644 --- a/src/inversify.config.ts +++ b/src/inversify.config.ts @@ -24,6 +24,7 @@ import Command from './commands'; import Clear from './commands/clear'; import Config from './commands/config'; import ForwardSeek from './commands/fseek'; +import Pause from './commands/pause'; import Play from './commands/play'; import QueueCommad from './commands/queue'; import Seek from './commands/seek'; @@ -45,6 +46,7 @@ container.bind(TYPES.Managers.Queue).to(QueueManager).inSingletonS container.bind(TYPES.Command).to(Clear).inSingletonScope(); container.bind(TYPES.Command).to(Config).inSingletonScope(); container.bind(TYPES.Command).to(ForwardSeek).inSingletonScope(); +container.bind(TYPES.Command).to(Pause).inSingletonScope(); container.bind(TYPES.Command).to(Play).inSingletonScope(); container.bind(TYPES.Command).to(QueueCommad).inSingletonScope(); container.bind(TYPES.Command).to(Seek).inSingletonScope(); diff --git a/src/services/player.ts b/src/services/player.ts index 3ce68ae..be2cdc0 100644 --- a/src/services/player.ts +++ b/src/services/player.ts @@ -20,8 +20,8 @@ export default class { private readonly cacheDir: string; private voiceConnection: VoiceConnection | null = null; private dispatcher: StreamDispatcher | null = null; + private playPositionInterval: NodeJS.Timeout | undefined; - private lastStreamTime = 0; private positionInSeconds = 0; constructor(queue: Queue, cacheDir: string) { @@ -37,6 +37,10 @@ export default class { disconnect(): void { if (this.voiceConnection) { + if (this.status === STATUS.PLAYING) { + this.pause(); + } + this.voiceConnection.disconnect(); } } @@ -54,9 +58,12 @@ export default class { await this.waitForCache(currentSong.url); - this.attachListeners(this.voiceConnection.play(this.getCachedPath(currentSong.url), {seek: positionSeconds})); + this.dispatcher = this.voiceConnection.play(this.getCachedPath(currentSong.url), {seek: positionSeconds}); - this.positionInSeconds = positionSeconds; + this.attachListeners(); + this.startTrackingPosition(positionSeconds); + + this.status = STATUS.PLAYING; } async forwardSeek(positionSeconds: number): Promise { @@ -73,9 +80,14 @@ export default class { } // Resume from paused state - if (this.status === STATUS.PAUSED && this.dispatcher) { - this.dispatcher.resume(); - this.status = STATUS.PLAYING; + if (this.status === STATUS.PAUSED) { + if (this.dispatcher) { + this.dispatcher.resume(); + this.status = STATUS.PLAYING; + } else { + await this.seek(this.getPosition()); + } + return; } @@ -85,27 +97,32 @@ export default class { throw new Error('Queue empty.'); } - let dispatcher: StreamDispatcher; - if (await this.isCached(currentSong.url)) { - dispatcher = this.voiceConnection.play(this.getCachedPath(currentSong.url)); + this.dispatcher = this.voiceConnection.play(this.getCachedPath(currentSong.url)); } else { const stream = await this.getStream(currentSong.url); - dispatcher = this.voiceConnection.play(stream, {type: 'webm/opus'}); + this.dispatcher = this.voiceConnection.play(stream, {type: 'webm/opus'}); } - this.attachListeners(dispatcher); + this.attachListeners(); this.status = STATUS.PLAYING; - this.dispatcher = dispatcher; + + this.startTrackingPosition(); } pause(): void { - if (!this.dispatcher || this.status !== STATUS.PLAYING) { + if (this.status !== STATUS.PLAYING) { throw new Error('Not currently playing.'); } - this.dispatcher.pause(); + this.status = STATUS.PAUSED; + + if (this.dispatcher) { + this.dispatcher.pause(); + } + + this.stopTrackingPosition(); } private getCurrentSong(): QueuedSong|null { @@ -235,12 +252,45 @@ export default class { return capacitor.createReadStream(); } - private attachListeners(stream: StreamDispatcher): void { - stream.on('speaking', async isSpeaking => { - // Update position - this.positionInSeconds += (stream.streamTime - this.lastStreamTime) / 1000; - this.lastStreamTime = stream.streamTime; + private startTrackingPosition(initalPosition?: number): void { + if (initalPosition) { + this.positionInSeconds = initalPosition; + } + if (this.playPositionInterval) { + clearInterval(this.playPositionInterval); + } + + this.playPositionInterval = setInterval(() => { + this.positionInSeconds++; + }, 1000); + } + + private stopTrackingPosition(): void { + if (this.playPositionInterval) { + clearInterval(this.playPositionInterval); + } + } + + private attachListeners(): void { + if (!this.voiceConnection) { + return; + } + + this.voiceConnection.on('disconnect', () => { + // Automatically pause + if (this.status === STATUS.PLAYING) { + this.pause(); + } + + this.dispatcher = null; + }); + + if (!this.dispatcher) { + return; + } + + this.dispatcher.on('speaking', async isSpeaking => { // Automatically advance queued song at end if (!isSpeaking && this.status === STATUS.PLAYING) { if (this.queue.get().length > 0) { @@ -249,12 +299,5 @@ export default class { } } }); - - stream.on('close', () => { - // Remove dispatcher from guild player - this.dispatcher = null; - - // TODO: set voiceConnection null as well? - }); } }