diff --git a/config.example.js b/config.example.js index f2fdb189..9649bf56 100644 --- a/config.example.js +++ b/config.example.js @@ -42,6 +42,10 @@ const config = { db_enabled: true, add_url_for_web_redirect: "", lavalink_default_search_prefix: "scsearch:", + search_extra_source_options: [{ + name: "Soundcloud", + value: "sc" + }], website_port: 10400, lavalink_nodes: [{ host: "localhost", diff --git a/packages/lang/localizations/de.json b/packages/lang/localizations/de.json index 041c07ea..a0dce28b 100644 --- a/packages/lang/localizations/de.json +++ b/packages/lang/localizations/de.json @@ -183,7 +183,8 @@ "GIVEN_TO_OTHER": "Du hast %user %amount gegeben.", "INVALID_DATA_TYPE_NO_DONOR_ARBITRARY": "The value you provided to update that setting with is invalid. Valid values are %acceptable", "INVALID_DATA_TYPE_YES_DONOR_ARBITRARY": "The value you provided to update that setting with is invalid. Donors are allowed to supply arbitrary values to this setting %link. Valid values are %acceptable", - "SETTING_UPDATED": "That setting was updated" + "SETTING_UPDATED": "That setting was updated", + "SONG_MOVED": "%title moved" }, "sit": { "name": "sit", @@ -736,5 +737,33 @@ "coupleleaderboard": { "name": "coupleleaderboard", "description": "Shows the leaderboard for top couples of money" + }, + "move": { + "name": "move", + "description": "Move a track in the queue to a new position", + "options": { + "from": { + "name": "from", + "description": "1 based index of the track to move" + }, + "to": { + "name": "to", + "description": "1 based index to move the track to" + } + } + }, + "search": { + "name": "search", + "description": "Search for a track from multiple sources", + "options": { + "input": { + "name": "input", + "description": "What to search for" + }, + "source": { + "name": "source", + "description": "The website to use to search for the track" + } + } } } diff --git a/packages/lang/localizations/en-gb.json b/packages/lang/localizations/en-gb.json index cf41e4d5..4f66c427 100644 --- a/packages/lang/localizations/en-gb.json +++ b/packages/lang/localizations/en-gb.json @@ -178,7 +178,8 @@ "GIVEN_TO_OTHER": "You have given %amount to %user", "INVALID_DATA_TYPE_NO_DONOR_ARBITRARY": "The value you provided to update that setting with is invalid. Valid values are %acceptable", "INVALID_DATA_TYPE_YES_DONOR_ARBITRARY": "The value you provided to update that setting with is invalid. Donors are allowed to supply arbitrary values to this setting %link. Valid values are %acceptable", - "SETTING_UPDATED": "That setting was updated" + "SETTING_UPDATED": "That setting was updated", + "SONG_MOVED": "%title moved" }, "sit": { "name": "sit", @@ -731,5 +732,33 @@ "coupleleaderboard": { "name": "coupleleaderboard", "description": "Shows the leaderboard for top couples of money" + }, + "move": { + "name": "move", + "description": "Move a track in the queue to a new position", + "options": { + "from": { + "name": "from", + "description": "1 based index of the track to move" + }, + "to": { + "name": "to", + "description": "1 based index to move the track to" + } + } + }, + "search": { + "name": "search", + "description": "Search for a track from multiple sources", + "options": { + "input": { + "name": "input", + "description": "What to search for" + }, + "source": { + "name": "source", + "description": "The website to use to search for the track" + } + } } } diff --git a/packages/lang/localizations/en-us.json b/packages/lang/localizations/en-us.json index 03d44f37..800ccd85 100644 --- a/packages/lang/localizations/en-us.json +++ b/packages/lang/localizations/en-us.json @@ -178,7 +178,8 @@ "GIVEN_TO_OTHER": "You have given %amount to %user", "INVALID_DATA_TYPE_NO_DONOR_ARBITRARY": "The value you provided to update that setting with is invalid. Valid values are %acceptable", "INVALID_DATA_TYPE_YES_DONOR_ARBITRARY": "The value you provided to update that setting with is invalid. Donors are allowed to supply arbitrary values to this setting %link. Valid values are %acceptable", - "SETTING_UPDATED": "That setting was updated" + "SETTING_UPDATED": "That setting was updated", + "SONG_MOVED": "%title moved" }, "sit": { "name": "sit", @@ -731,5 +732,33 @@ "coupleleaderboard": { "name": "coupleleaderboard", "description": "Shows the leaderboard for top couples of money" + }, + "move": { + "name": "move", + "description": "Move a track in the queue to a new position", + "options": { + "from": { + "name": "from", + "description": "1 based index of the track to move" + }, + "to": { + "name": "to", + "description": "1 based index to move the track to" + } + } + }, + "search": { + "name": "search", + "description": "Search for a track from multiple sources", + "options": { + "input": { + "name": "input", + "description": "What to search for" + }, + "source": { + "name": "source", + "description": "The website to use to search for the track" + } + } } } diff --git a/packages/lang/localizations/es-es.json b/packages/lang/localizations/es-es.json index 837bf62f..fd41c4cd 100644 --- a/packages/lang/localizations/es-es.json +++ b/packages/lang/localizations/es-es.json @@ -178,7 +178,8 @@ "GIVEN_TO_OTHER": "You have given %amount to %user", "INVALID_DATA_TYPE_NO_DONOR_ARBITRARY": "The value you provided to update that setting with is invalid. Valid values are %acceptable", "INVALID_DATA_TYPE_YES_DONOR_ARBITRARY": "The value you provided to update that setting with is invalid. Donors are allowed to supply arbitrary values to this setting %link. Valid values are %acceptable", - "SETTING_UPDATED": "That setting was updated" + "SETTING_UPDATED": "That setting was updated", + "SONG_MOVED": "%title moved" }, "sit": { "name": "sentarse", @@ -731,5 +732,33 @@ "coupleleaderboard": { "name": "coupleleaderboard", "description": "Shows the leaderboard for top couples of money" + }, + "move": { + "name": "move", + "description": "Move a track in the queue to a new position", + "options": { + "from": { + "name": "from", + "description": "1 based index of the track to move" + }, + "to": { + "name": "to", + "description": "1 based index to move the track to" + } + } + }, + "search": { + "name": "search", + "description": "Search for a track from multiple sources", + "options": { + "input": { + "name": "input", + "description": "What to search for" + }, + "source": { + "name": "source", + "description": "The website to use to search for the track" + } + } } } diff --git a/packages/lang/localizations/nl.json b/packages/lang/localizations/nl.json index a5d43953..42abb0ed 100644 --- a/packages/lang/localizations/nl.json +++ b/packages/lang/localizations/nl.json @@ -183,7 +183,8 @@ "GIVEN_TO_OTHER": "You have given %amount to %user", "INVALID_DATA_TYPE_NO_DONOR_ARBITRARY": "The value you provided to update that setting with is invalid. Valid values are %acceptable", "INVALID_DATA_TYPE_YES_DONOR_ARBITRARY": "The value you provided to update that setting with is invalid. Donors are allowed to supply arbitrary values to this setting %link. Valid values are %acceptable", - "SETTING_UPDATED": "That setting was updated" + "SETTING_UPDATED": "That setting was updated", + "SONG_MOVED": "%title moved" }, "sit": { "name": "zit", @@ -736,5 +737,33 @@ "coupleleaderboard": { "name": "coupleleaderboard", "description": "Shows the leaderboard for top couples of money" + }, + "move": { + "name": "move", + "description": "Move a track in the queue to a new position", + "options": { + "from": { + "name": "from", + "description": "1 based index of the track to move" + }, + "to": { + "name": "to", + "description": "1 based index to move the track to" + } + } + }, + "search": { + "name": "search", + "description": "Search for a track from multiple sources", + "options": { + "input": { + "name": "input", + "description": "What to search for" + }, + "source": { + "name": "source", + "description": "The website to use to search for the track" + } + } } } diff --git a/packages/lang/localizations/pl.json b/packages/lang/localizations/pl.json index 840d217f..5e62546f 100644 --- a/packages/lang/localizations/pl.json +++ b/packages/lang/localizations/pl.json @@ -178,7 +178,8 @@ "GIVEN_TO_OTHER": "You have given %amount to %user", "INVALID_DATA_TYPE_NO_DONOR_ARBITRARY": "The value you provided to update that setting with is invalid. Valid values are %acceptable", "INVALID_DATA_TYPE_YES_DONOR_ARBITRARY": "The value you provided to update that setting with is invalid. Donors are allowed to supply arbitrary values to this setting %link. Valid values are %acceptable", - "SETTING_UPDATED": "That setting was updated" + "SETTING_UPDATED": "That setting was updated", + "SONG_MOVED": "%title moved" }, "sit": { "name": "siad", @@ -731,5 +732,33 @@ "coupleleaderboard": { "name": "coupleleaderboard", "description": "Shows the leaderboard for top couples of money" + }, + "move": { + "name": "move", + "description": "Move a track in the queue to a new position", + "options": { + "from": { + "name": "from", + "description": "1 based index of the track to move" + }, + "to": { + "name": "to", + "description": "1 based index to move the track to" + } + } + }, + "search": { + "name": "search", + "description": "Search for a track from multiple sources", + "options": { + "input": { + "name": "input", + "description": "What to search for" + }, + "source": { + "name": "source", + "description": "The website to use to search for the track" + } + } } } diff --git a/packages/lang/localizations/ru.json b/packages/lang/localizations/ru.json index 95cef0f4..b247fa6a 100644 --- a/packages/lang/localizations/ru.json +++ b/packages/lang/localizations/ru.json @@ -178,7 +178,8 @@ "GIVEN_TO_OTHER": "You have given %amount to %user", "INVALID_DATA_TYPE_NO_DONOR_ARBITRARY": "The value you provided to update that setting with is invalid. Valid values are %acceptable", "INVALID_DATA_TYPE_YES_DONOR_ARBITRARY": "The value you provided to update that setting with is invalid. Donors are allowed to supply arbitrary values to this setting %link. Valid values are %acceptable", - "SETTING_UPDATED": "That setting was updated" + "SETTING_UPDATED": "That setting was updated", + "SONG_MOVED": "%title moved" }, "sit": { "name": "сидеть", @@ -731,5 +732,33 @@ "coupleleaderboard": { "name": "coupleleaderboard", "description": "Shows the leaderboard for top couples of money" + }, + "move": { + "name": "move", + "description": "Move a track in the queue to a new position", + "options": { + "from": { + "name": "from", + "description": "1 based index of the track to move" + }, + "to": { + "name": "to", + "description": "1 based index to move the track to" + } + } + }, + "search": { + "name": "search", + "description": "Search for a track from multiple sources", + "options": { + "input": { + "name": "input", + "description": "What to search for" + }, + "source": { + "name": "source", + "description": "The website to use to search for the track" + } + } } } diff --git a/packages/runtime-website/src/music/music.ts b/packages/runtime-website/src/music/music.ts index a6d0d099..d93e18bd 100644 --- a/packages/runtime-website/src/music/music.ts +++ b/packages/runtime-website/src/music/music.ts @@ -601,5 +601,116 @@ commands.assign([ } } } + }, + { + name: "move", + description: "Move a track in the queue to a new position", + category: "audio", + options: [ + { + name: "from", + type: 4, + description: "1 based index of the track to move", + required: true, + min_value: 2 + }, + { + name: "to", + type: 4, + description: "1 based index to move the track to", + required: true, + min_value: 2 + } + ], + async process(cmd, lang) { + if (!common.queues.doChecks(cmd, lang)) return + + const queue = common.queues.getQueueWithRequiredPresence(cmd, lang) + if (!queue) return + + const fromOption = cmd.data.options.get("from")!.asNumber()! + const toOption = cmd.data.options.get("to")!.asNumber()! + + const track = queue.tracks[fromOption - 1] + + if (fromOption > queue.tracks.length || toOption > queue.tracks.length || !track) { + return snow.interaction.editOriginalInteractionResponse(cmd.application_id, cmd.token, { + content: lang.GLOBAL.OUT_OF_BOUNDS + }) + } + + await queue.removeTrack(fromOption - 1) + queue.addTrack(track, toOption - 1) + return snow.interaction.editOriginalInteractionResponse(cmd.application_id, cmd.token, { + content: langReplace(lang.GLOBAL.SONG_MOVED, { "title": track.title }) + }) + } + }, + { + name: "search", + description: "Search for a track from multiple sources", + category: "audio", + options: [ + { + name: "input", + type: 3, + description: "What to search for", + required: true + }, + { + name: "source", + type: 3, + description: "The website to use to search for the track", + required: false, + choices: [ + { + name: "Spotify", + value: "sp" + }, + { + name: "Apple Music", + value: "am" + }, + ...confprovider.config.search_extra_source_options + ] + } + ], + async process(cmd, lang) { + if (!common.queues.doChecks(cmd, lang)) return + + const queue = queues.get(cmd.guild_id!) + + const input = cmd.data.options.get("input")!.asString()! + const source = cmd.data.options.get("source")?.asString() + + const prefix = source ? `${source}search:` : confprovider.config.lavalink_default_search_prefix + + let tracks: Awaited> | undefined = void 0 + try { + tracks = await common.loadtracks(`${prefix}${input}`, lang, queue?.node) + } catch (e) { + return common.handleTrackLoadError(cmd, e, input) + } + + const mapped = common.handleTrackLoadsToArray(tracks) + + if (!mapped) { + return snow.interaction.editOriginalInteractionResponse(cmd.application_id, cmd.token, { + content: lang.GLOBAL.NO_RESULTS, + embeds: [] + }) + } + + snow.interaction.editOriginalInteractionResponse(cmd.application_id, cmd.token, { + embeds: [ + { + color: confprovider.config.standard_embed_color, + description: mapped + .map(track => `[${track.info.author} - ${track.info.title}](${track.info.uri}) (${sharedUtils.prettySeconds(Math.round(Number(track.info.length) / 1000))})`) + .join("\n").slice(0, 1998) + } + ] + }) + } } ]) diff --git a/packages/runtime-website/src/music/utils.ts b/packages/runtime-website/src/music/utils.ts index 597cd7fd..41b61e9b 100644 --- a/packages/runtime-website/src/music/utils.ts +++ b/packages/runtime-website/src/music/utils.ts @@ -10,7 +10,7 @@ import type { ChatInputCommand } from "@amanda/commands" import type { Lang } from "@amanda/lang" import type { Track } from "./tracktypes" import type { APIEmbed, APIUser } from "discord-api-types/v10" -import type { TrackLoadingResult, TrackInfo } from "lavalink-types/v4" +import type { TrackLoadingResult, TrackInfo, Track as LLTrack } from "lavalink-types/v4" import type { Queue } from "./queue" import passthrough = require("../passthrough") @@ -30,7 +30,7 @@ const replaceExtraneousRegex = / ?\([^)]+\) ?/g type Key = Exclude const sourceMap = new Map([ - ["itunes", "RequiresSearchTrack"], + ["applemusic", "RequiresSearchTrack"], ["spotify", "RequiresSearchTrack"], ["http", "ExternalTrack"] ]) @@ -70,18 +70,28 @@ const common = { pickApart(track: import("./tracktypes").Track) { let title = "", artist: string | undefined = undefined let confidence = 0 + let skip = false - const authorNameMatch = knownGoodArtistRegex.exec(track.author) - const trackNameMatch = trackNameRegex.exec(track.title) - - if (authorNameMatch) { - title = track.title?.replace(replaceExtraneousRegex, "")?.trim() - artist = authorNameMatch[1]?.trim() + if (track.source === "spotify" || track.source === "applemusic") { confidence = 2 - } else if (trackNameMatch) { - title = trackNameMatch[2]?.trim() - artist = trackNameMatch[1]?.trim() - confidence = 1 // mostly confident. Could just flip around + title = track.title + artist = track.author + skip = true + } + + if (!skip) { + const authorNameMatch = knownGoodArtistRegex.exec(track.author) + const trackNameMatch = trackNameRegex.exec(track.title) + + if (authorNameMatch) { + title = track.title?.replace(replaceExtraneousRegex, "")?.trim() + artist = authorNameMatch[1]?.trim() + confidence = 2 + } else if (trackNameMatch) { + title = trackNameMatch[2]?.trim() + artist = trackNameMatch[1]?.trim() + confidence = 1 // mostly confident. Could just flip around + } } if (!title || !artist) { @@ -93,52 +103,65 @@ const common = { } }, + handleTrackLoadError(cmd: ChatInputCommand, error: LoadTracksError, input: string) { + const reportTarget = confprovider.config.error_log_channel_id + const undef = "undefined" + + const details = [ + ["Tree", confprovider.config.cluster_id], + ["Branch", "music"], + ["Node", error.node], + ["User", sharedUtils.userString(cmd.author)], + ["User ID", cmd.author.id], + ["Guild ID", cmd.guild_id ?? undef], + ["Text Channel", cmd.channel.id], + ["Input", input] + ] + + const maxLength = details.reduce((page, c) => Math.max(page, c[0].length), 0) + const detailsString = details.map(row => + `\`${row[0]}${" ​".repeat(maxLength - row[0].length)}\` ${row[1]}` // SC: space + zwsp, wide space + ).join("\n") + + const embed: APIEmbed = { + title: "LavaLink loadtracks exception", + color: 0xdd2d2d, + fields: [ + { name: "Details", value: detailsString }, + { name: "Exception", value: error.message || undef } + ] + } + + snow.interaction.editOriginalInteractionResponse(cmd.application_id, cmd.token, { + content: error.message ?? "A load tracks exception occured, but no error message was provided", + embeds: [] + }) + + snow.channel.createMessage(reportTarget, { embeds: [embed] }) + }, + + handleTrackLoadsToArray(tracks: TrackLoadingResult): Array | null { + if (tracks.loadType === "empty" || tracks.loadType === "error") return null + + if (tracks.loadType === "track") return [tracks.data] + else if (tracks.loadType === "playlist") return tracks.data.tracks + else return tracks.data + }, + async inputToTrack(resource: string, cmd: ChatInputCommand, lang: Lang, node?: string): Promise | null> { resource = resource.replace(hiddenEmbedRegex, "") let tracks: Awaited> | undefined = void 0 try { tracks = await common.loadtracks(resource, lang, node) - } catch (er) { - const e: LoadTracksError = er - const reportTarget = confprovider.config.error_log_channel_id - const undef = "undefined" - - const details = [ - ["Tree", confprovider.config.cluster_id], - ["Branch", "music"], - ["Node", e.node], - ["User", sharedUtils.userString(cmd.author)], - ["User ID", cmd.author.id], - ["Guild ID", cmd.guild_id ?? undef], - ["Text Channel", cmd.channel.id], - ["Input", resource] - ] - - const maxLength = details.reduce((page, c) => Math.max(page, c[0].length), 0) - const detailsString = details.map(row => - `\`${row[0]}${" ​".repeat(maxLength - row[0].length)}\` ${row[1]}` // SC: space + zwsp, wide space - ).join("\n") - - const embed: APIEmbed = { - title: "LavaLink loadtracks exception", - color: 0xdd2d2d, - fields: [ - { name: "Details", value: detailsString }, - { name: "Exception", value: e.message || undef } - ] - } - - snow.interaction.editOriginalInteractionResponse(cmd.application_id, cmd.token, { - content: e.message ?? "A load tracks exception occured, but no error message was provided", - embeds: [] - }) - - snow.channel.createMessage(reportTarget, { embeds: [embed] }) + } catch (e) { + common.handleTrackLoadError(cmd, e, resource) return null } - if (tracks.loadType === "empty" || tracks.loadType === "error") { + const mapped = common.handleTrackLoadsToArray(tracks) + + if (!mapped) { snow.interaction.editOriginalInteractionResponse(cmd.application_id, cmd.token, { content: lang.GLOBAL.NO_RESULTS, embeds: [] @@ -146,18 +169,9 @@ const common = { return null } - if (tracks.loadType === "track") { - return [ - decodedToTrack( - tracks.data.encoded, - tracks.data.info, - resource, - cmd.author, - sharedUtils.getLang(cmd.guild_locale!) - ) - ] - } else if (tracks.loadType === "playlist") { - return tracks.data.tracks.map(track => decodedToTrack( + + if (tracks.loadType !== "search") { + return mapped.map(track => decodedToTrack( track.encoded, track.info, resource, @@ -170,7 +184,7 @@ const common = { cmd, lang, tracks.data, - i => `${i.info.author} - ${i.info.title} (${sharedUtils.prettySeconds(Math.round(Number(i.info.length) / 1000))})` + i => `[${i.info.author} - ${i.info.title}](${i.info.uri}) (${sharedUtils.prettySeconds(Math.round(Number(i.info.length) / 1000))})` ) if (!chosen) {