diff --git a/CHANGELOG.md b/CHANGELOG.md index 5bc2ec8..97f2d71 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Changed - Now uses [esmo](https://github.com/antfu/esno) so we don't have to build +- `/seek` and `/fseek` can now be given duration strings. For example, `1m` and `2m 15s` work. If the input consists only of numbers, Muse will treat it as the number of seconds to advance (backwards-compatible behavior). ## [1.5.0] - 2022-03-12 ### Changed diff --git a/package.json b/package.json index c0ea806..ce04e8f 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,9 @@ "engines": { "node": ">=16.0.0" }, - "files": ["src"], + "files": [ + "src" + ], "scripts": { "lint": "eslint \"src/**/*.{ts,tsx}\"", "lint:fix": "eslint \"src/**/*.{ts,tsx}\" --fix", @@ -31,6 +33,7 @@ "@types/debug": "^4.1.5", "@types/fluent-ffmpeg": "^2.1.17", "@types/fs-capacitor": "^2.0.0", + "@types/ms": "0.7.31", "@types/node": "^17.0.0", "@types/node-emoji": "^1.8.1", "@types/spotify-web-api-node": "^5.0.2", @@ -100,6 +103,7 @@ "p-event": "^5.0.1", "p-limit": "^4.0.0", "p-queue": "^7.2.0", + "parse-duration": "1.0.2", "read-pkg": "7.1.0", "reflect-metadata": "^0.1.13", "spotify-uri": "^2.2.0", diff --git a/src/commands/fseek.ts b/src/commands/fseek.ts index 985a7c4..3e34438 100644 --- a/src/commands/fseek.ts +++ b/src/commands/fseek.ts @@ -5,15 +5,16 @@ import {inject, injectable} from 'inversify'; import PlayerManager from '../managers/player.js'; import Command from '.'; import {prettyTime} from '../utils/time.js'; +import durationStringToSeconds from '../utils/duration-string-to-seconds.js'; @injectable() export default class implements Command { public readonly slashCommand = new SlashCommandBuilder() .setName('fseek') .setDescription('seek forward in the current song') - .addNumberOption(option => option - .setName('seconds') - .setDescription('the number of seconds to skip forward') + .addStringOption(option => option + .setName('time') + .setDescription('an interval expression or number of seconds (1m, 30s, 100)') .setRequired(true)); public requiresVC = true; @@ -37,12 +38,14 @@ export default class implements Command { throw new Error('can\'t seek in a livestream'); } - const seekTime = interaction.options.getNumber('seconds'); + const seekValue = interaction.options.getString('value'); - if (!seekTime) { - throw new Error('missing number of seconds to seek'); + if (!seekValue) { + throw new Error('missing seek value'); } + const seekTime = durationStringToSeconds(seekValue); + if (seekTime + player.getPosition() > currentSong.length) { throw new Error('can\'t seek past the end of the song'); } diff --git a/src/commands/seek.ts b/src/commands/seek.ts index 07f6590..00e9c2e 100644 --- a/src/commands/seek.ts +++ b/src/commands/seek.ts @@ -5,6 +5,7 @@ import PlayerManager from '../managers/player.js'; import Command from '.'; import {parseTime, prettyTime} from '../utils/time.js'; import {SlashCommandBuilder} from '@discordjs/builders'; +import durationStringToSeconds from '../utils/duration-string-to-seconds.js'; @injectable() export default class implements Command { @@ -13,7 +14,7 @@ export default class implements Command { .setDescription('seek to a position from beginning of song') .addStringOption(option => option.setName('time') - .setDescription('time to seek') + .setDescription('an interval expression or number of seconds (1m, 30s, 100)') .setRequired(true), ); @@ -45,7 +46,7 @@ export default class implements Command { if (time.includes(':')) { seekTime = parseTime(time); } else { - seekTime = parseInt(time, 10); + seekTime = durationStringToSeconds(time); } if (seekTime > currentSong.length) { diff --git a/src/services/player.ts b/src/services/player.ts index 6e99364..b3ed34d 100644 --- a/src/services/player.ts +++ b/src/services/player.ts @@ -5,7 +5,17 @@ import ytdl from 'ytdl-core'; import {WriteStream} from 'fs-capacitor'; import ffmpeg from 'fluent-ffmpeg'; import shuffle from 'array-shuffle'; -import {AudioPlayer, AudioPlayerStatus, createAudioPlayer, createAudioResource, joinVoiceChannel, StreamType, VoiceConnection, VoiceConnectionStatus} from '@discordjs/voice'; +import { + AudioPlayer, + AudioPlayerState, + AudioPlayerStatus, + createAudioPlayer, + createAudioResource, + joinVoiceChannel, + StreamType, + VoiceConnection, + VoiceConnectionStatus, +} from '@discordjs/voice'; import FileCacheProvider from './file-cache.js'; import debug from '../utils/debug.js'; import {prisma} from '../utils/db.js'; @@ -493,7 +503,7 @@ export default class { } if (this.audioPlayer.listeners('stateChange').length === 0) { - this.audioPlayer.on('stateChange', this.onAudioPlayerStateChange.bind(this)); + this.audioPlayer.on(AudioPlayerStatus.Idle, this.onAudioPlayerIdle.bind(this)); } } @@ -501,7 +511,7 @@ export default class { this.disconnect(); } - private async onAudioPlayerStateChange(_oldState: {status: AudioPlayerStatus}, newState: {status: AudioPlayerStatus}): Promise { + private async onAudioPlayerIdle(_oldState: AudioPlayerState, newState: AudioPlayerState): Promise { // Automatically advance queued song at end if (newState.status === AudioPlayerStatus.Idle && this.status === STATUS.PLAYING) { await this.forward(1); diff --git a/src/utils/duration-string-to-seconds.ts b/src/utils/duration-string-to-seconds.ts new file mode 100644 index 0000000..588ba5b --- /dev/null +++ b/src/utils/duration-string-to-seconds.ts @@ -0,0 +1,21 @@ +import parse from 'parse-duration'; + +/** + * Parse duration strings to seconds. + * @param str any common duration format, like 1m or 1hr 30s. If the input is a number it's assumed to be in seconds. + * @returns seconds + */ +const durationStringToSeconds = (str: string) => { + let seconds; + const isInputSeconds = Boolean(/\d+$/.exec(str)); + + if (isInputSeconds) { + seconds = Number.parseInt(str, 10); + } else { + seconds = parse(str) / 1000; + } + + return seconds; +}; + +export default durationStringToSeconds; diff --git a/yarn.lock b/yarn.lock index 3f03253..3090dc2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -384,7 +384,7 @@ resolved "https://registry.yarnpkg.com/@types/libsodium-wrappers/-/libsodium-wrappers-0.7.9.tgz#89c3ad2156d5143e64bce86cfeb0045a983aeccc" integrity sha512-LisgKLlYQk19baQwjkBZZXdJL0KbeTpdEnrAfz5hQACbklCY0gVFnsKUyjfNWF1UQsCSjw93Sj5jSbiO8RPfdw== -"@types/ms@*": +"@types/ms@*", "@types/ms@0.7.31": version "0.7.31" resolved "https://registry.yarnpkg.com/@types/ms/-/ms-0.7.31.tgz#31b7ca6407128a3d2bbc27fe2d21b345397f6197" integrity sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA== @@ -2855,6 +2855,11 @@ parent-module@^1.0.0: dependencies: callsites "^3.0.0" +parse-duration@1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/parse-duration/-/parse-duration-1.0.2.tgz#b9aa7d3a1363cc7e8845bea8fd3baf8a11df5805" + integrity sha512-Dg27N6mfok+ow1a2rj/nRjtCfaKrHUZV2SJpEn/s8GaVUSlf4GGRCRP1c13Hj+wfPKVMrFDqLMLITkYKgKxyyg== + parse-json@5.2.0, parse-json@^5.0.0, parse-json@^5.2.0: version "5.2.0" resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-5.2.0.tgz#c76fc66dee54231c962b22bcc8a72cf2f99753cd"