Skip to content

Commit

Permalink
feat: Add /music fast-forward and /music rewind; fix `/music skip…
Browse files Browse the repository at this point in the history
…-to` command.
  • Loading branch information
vxern committed Dec 26, 2023
1 parent c6116dd commit 7b1127b
Show file tree
Hide file tree
Showing 9 changed files with 331 additions and 27 deletions.
16 changes: 16 additions & 0 deletions assets/localisations/commands/eng-US.json
Original file line number Diff line number Diff line change
Expand Up @@ -521,6 +521,22 @@
"music.options.skip-to.strings.invalidTimestamp.description": "The timestamp you specified is invalid.",
"music.options.skip-to.strings.skippedTo.title": "Skipped to timestamp",
"music.options.skip-to.strings.skippedTo.description": "Playback has skipped to the specified timestamp.",
"music.options.fast-forward.name": "fast-forward",
"music.options.fast-forward.description": "Fast-forwards the song by a given amount of time.",
"music.options.fast-forward.strings.noSong.title": "No song to fast-forward",
"music.options.fast-forward.strings.noSong.description": "There is no song to fast-forward.",
"music.options.fast-forward.strings.invalidTimestamp.title": "Invalid timestamp",
"music.options.fast-forward.strings.invalidTimestamp.description": "The timestamp you specified is invalid.",
"music.options.fast-forward.strings.fastForwarded.title": "Fast-forwarded",
"music.options.fast-forward.strings.fastForwarded.description": "Playback has skipped by the given amount of time.",
"music.options.rewind.name": "rewind",
"music.options.rewind.description": "Rewinds the song by a given amount of time.",
"music.options.rewind.strings.noSong.title": "No song to rewind",
"music.options.rewind.strings.noSong.description": "There is no song to rewind.",
"music.options.rewind.strings.invalidTimestamp.title": "Invalid timestamp",
"music.options.rewind.strings.invalidTimestamp.description": "The timestamp you specified is invalid.",
"music.options.rewind.strings.rewound.title": "Rewound",
"music.options.rewind.strings.rewound.description": "Playback has been rewound by the given amount of time.",
"music.options.skip.name": "skip",
"music.options.skip.description": "Skips the currently playing song.",
"music.options.skip.strings.noSong.title": "No song to skip",
Expand Down
2 changes: 1 addition & 1 deletion assets/localisations/parameters/eng-US.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
"parameters.query.name": "query",
"parameters.query.description": "The title or a link to the song or song collection.",
"parameters.timestamp.name": "timestamp",
"parameters.timestamp.description": "The timestamp to seek.",
"parameters.timestamp.description": "The timestamp to use for seeking.",
"parameters.collection.name": "collection",
"parameters.collection.description": "If set to true, the action will be taken on the song collection instead.",
"parameters.by.name": "by",
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"name": "logos",
"description": "A multi-purpose community bot built to cater to language-learning communities on Discord.",
"license": "ISC",
"version": "3.39.1",
"version": "3.40.0",
"type": "module",
"keywords": [
"discord",
Expand Down
2 changes: 2 additions & 0 deletions src/constants/types/symbols.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,8 @@ export default {
resumed: "▶️",
skippedTo: "🔍",
skipped: "⏭️",
fastForwarded: "⏩",
rewound: "⏪",
stopped: "⏹️",
unskipped: "⏮️",
volume: "🔊",
Expand Down
23 changes: 21 additions & 2 deletions src/lib/commands/music/commands.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import * as Discord from "@discordeno/bot";
import { CommandTemplate } from "../command";
import fastForward from "./commands/fast-forward";
import history from "./commands/history";
import loop from "./commands/loop";
import now from "./commands/now";
Expand All @@ -9,8 +10,9 @@ import queue from "./commands/queue";
import remove from "./commands/remove";
import replay from "./commands/replay";
import resume from "./commands/resume";
import skip from "./commands/skip";
import rewind from "./commands/rewind";
import skipTo from "./commands/skip-to";
import skip from "./commands/skip";
import stop from "./commands/stop";
import unskip from "./commands/unskip";
import volume from "./commands/volume";
Expand All @@ -19,7 +21,24 @@ const music: CommandTemplate = {
name: "music",
type: Discord.ApplicationCommandTypes.ChatInput,
defaultMemberPermissions: ["VIEW_CHANNEL"],
options: [history, loop, now, pause, play, queue, remove, replay, resume, skipTo, skip, stop, unskip, volume],
options: [
fastForward,
history,
loop,
now,
pause,
play,
queue,
remove,
replay,
resume,
rewind,
skipTo,
skip,
stop,
unskip,
volume,
],
};

export { music };
132 changes: 132 additions & 0 deletions src/lib/commands/music/commands/fast-forward.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import * as Discord from "@discordeno/bot";
import constants from "../../../../constants/constants";
import { Locale } from "../../../../constants/languages";
import { trim } from "../../../../formatting";
import * as Logos from "../../../../types";
import { Client, localise } from "../../../client";
import { parseArguments, parseTimeExpression, reply, respond } from "../../../interactions";
import { OptionTemplate } from "../../command";
import { timestamp } from "../../parameters";

const command: OptionTemplate = {
name: "fast-forward",
type: Discord.ApplicationCommandOptionTypes.SubCommand,
handle: handleFastForward,
handleAutocomplete: handleFastForwardAutocomplete,
options: [timestamp],
};

async function handleFastForwardAutocomplete(
[client, bot]: [Client, Discord.Bot],
interaction: Logos.Interaction,
): Promise<void> {
const language = interaction.language;
const locale = interaction.locale;

const [{ timestamp: timestampExpression }] = parseArguments(interaction.data?.options, {});
if (timestampExpression === undefined) {
return;
}

const timestamp = parseTimeExpression(client, timestampExpression, { language, locale });
if (timestamp === undefined) {
const strings = {
autocomplete: localise(client, "autocomplete.timestamp", locale)(),
};

respond([client, bot], interaction, [{ name: trim(strings.autocomplete, 100), value: "" }]);
return;
}

respond([client, bot], interaction, [{ name: timestamp[0], value: timestamp[1].toString() }]);
}

async function handleFastForward([client, bot]: [Client, Discord.Bot], interaction: Logos.Interaction): Promise<void> {
const locale = interaction.guildLocale;

const [{ timestamp: timestampExpression }] = parseArguments(interaction.data?.options, {});

const guildId = interaction.guildId;
if (guildId === undefined) {
return;
}

const musicService = client.services.music.music.get(guildId);
if (musicService === undefined) {
return;
}

const isVoiceStateVerified = musicService.verifyCanManagePlayback(interaction);
if (!isVoiceStateVerified) {
return;
}

const [isOccupied, current, position] = [musicService.isOccupied, musicService.current, musicService.position];
if (!isOccupied || current === undefined) {
const locale = interaction.locale;
const strings = {
title: localise(client, "music.options.fast-forward.strings.noSong.title", locale)(),
description: localise(client, "music.options.fast-forward.strings.noSong.description", locale)(),
};

reply([client, bot], interaction, {
embeds: [
{
title: strings.title,
description: strings.description,
color: constants.colors.dullYellow,
},
],
});
return;
}

if (position === undefined) {
return;
}

const timestamp = Number(timestampExpression);
if (!Number.isSafeInteger(timestamp)) {
displayInvalidTimestampError([client, bot], interaction, { locale });
return;
}

await musicService.skipTo(Math.round((position + timestamp) / 1000) * 1000);

const strings = {
title: localise(client, "music.options.fast-forward.strings.fastForwarded.title", locale)(),
description: localise(client, "music.options.fast-forward.strings.fastForwarded.description", locale)(),
};

reply(
[client, bot],
interaction,
{
embeds: [
{
title: `${constants.symbols.music.fastForwarded} ${strings.title}`,
description: strings.description,
color: constants.colors.blue,
},
],
},
{ visible: true },
);
}

async function displayInvalidTimestampError(
[client, bot]: [Client, Discord.Bot],
interaction: Logos.Interaction,
{ locale }: { locale: Locale },
): Promise<void> {
const strings = {
title: localise(client, "music.options.fast-forward.strings.invalidTimestamp.title", locale)(),
description: localise(client, "music.options.fast-forward.strings.invalidTimestamp.description", locale)(),
};

reply([client, bot], interaction, {
embeds: [{ title: strings.title, description: strings.description, color: constants.colors.red }],
});
}

export default command;
132 changes: 132 additions & 0 deletions src/lib/commands/music/commands/rewind.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import * as Discord from "@discordeno/bot";
import constants from "../../../../constants/constants";
import { Locale } from "../../../../constants/languages";
import { trim } from "../../../../formatting";
import * as Logos from "../../../../types";
import { Client, localise } from "../../../client";
import { parseArguments, parseTimeExpression, reply, respond } from "../../../interactions";
import { OptionTemplate } from "../../command";
import { timestamp } from "../../parameters";

const command: OptionTemplate = {
name: "rewind",
type: Discord.ApplicationCommandOptionTypes.SubCommand,
handle: handleRewind,
handleAutocomplete: handleRewindAutocomplete,
options: [timestamp],
};

async function handleRewindAutocomplete(
[client, bot]: [Client, Discord.Bot],
interaction: Logos.Interaction,
): Promise<void> {
const language = interaction.language;
const locale = interaction.locale;

const [{ timestamp: timestampExpression }] = parseArguments(interaction.data?.options, {});
if (timestampExpression === undefined) {
return;
}

const timestamp = parseTimeExpression(client, timestampExpression, { language, locale });
if (timestamp === undefined) {
const strings = {
autocomplete: localise(client, "autocomplete.timestamp", locale)(),
};

respond([client, bot], interaction, [{ name: trim(strings.autocomplete, 100), value: "" }]);
return;
}

respond([client, bot], interaction, [{ name: timestamp[0], value: timestamp[1].toString() }]);
}

async function handleRewind([client, bot]: [Client, Discord.Bot], interaction: Logos.Interaction): Promise<void> {
const locale = interaction.guildLocale;

const [{ timestamp: timestampExpression }] = parseArguments(interaction.data?.options, {});

const guildId = interaction.guildId;
if (guildId === undefined) {
return;
}

const musicService = client.services.music.music.get(guildId);
if (musicService === undefined) {
return;
}

const isVoiceStateVerified = musicService.verifyCanManagePlayback(interaction);
if (!isVoiceStateVerified) {
return;
}

const [isOccupied, current, position] = [musicService.isOccupied, musicService.current, musicService.position];
if (!isOccupied || current === undefined) {
const locale = interaction.locale;
const strings = {
title: localise(client, "music.options.rewind.strings.noSong.title", locale)(),
description: localise(client, "music.options.rewind.strings.noSong.description", locale)(),
};

reply([client, bot], interaction, {
embeds: [
{
title: strings.title,
description: strings.description,
color: constants.colors.dullYellow,
},
],
});
return;
}

if (position === undefined) {
return;
}

const timestamp = Number(timestampExpression);
if (!Number.isSafeInteger(timestamp)) {
displayInvalidTimestampError([client, bot], interaction, { locale });
return;
}

await musicService.skipTo(Math.round((position - timestamp) / 1000) * 1000);

const strings = {
title: localise(client, "music.options.rewind.strings.rewound.title", locale)(),
description: localise(client, "music.options.rewind.strings.rewound.description", locale)(),
};

reply(
[client, bot],
interaction,
{
embeds: [
{
title: `${constants.symbols.music.rewound} ${strings.title}`,
description: strings.description,
color: constants.colors.blue,
},
],
},
{ visible: true },
);
}

async function displayInvalidTimestampError(
[client, bot]: [Client, Discord.Bot],
interaction: Logos.Interaction,
{ locale }: { locale: Locale },
): Promise<void> {
const strings = {
title: localise(client, "music.options.rewind.strings.invalidTimestamp.title", locale)(),
description: localise(client, "music.options.rewind.strings.invalidTimestamp.description", locale)(),
};

reply([client, bot], interaction, {
embeds: [{ title: strings.title, description: strings.description, color: constants.colors.red }],
});
}

export default command;
Loading

0 comments on commit 7b1127b

Please sign in to comment.