diff --git a/src/Apis/Genius/GeniusSong.cs b/src/Apis/Genius/GeniusSong.cs
index 66396a9..4cd202e 100644
--- a/src/Apis/Genius/GeniusSong.cs
+++ b/src/Apis/Genius/GeniusSong.cs
@@ -6,6 +6,7 @@ namespace Fergun.Apis.Genius;
///
public record GeniusSong(
[property: JsonPropertyName("artist_names")] string ArtistNames,
+ [property: JsonPropertyName("primary_artist_names")] string PrimaryArtistNames,
[property: JsonPropertyName("id")] int Id,
[property: JsonPropertyName("instrumental")] bool IsInstrumental,
[property: JsonPropertyName("lyrics_state")] string LyricsState,
diff --git a/src/Apis/Genius/IGeniusSong.cs b/src/Apis/Genius/IGeniusSong.cs
index 052cb77..28b5227 100644
--- a/src/Apis/Genius/IGeniusSong.cs
+++ b/src/Apis/Genius/IGeniusSong.cs
@@ -9,6 +9,11 @@ public interface IGeniusSong
/// Gets the artist names.
///
string ArtistNames { get; }
+
+ ///
+ /// Gets the primary artist names.
+ ///
+ string PrimaryArtistNames { get; }
///
/// Gets the ID of this song.
diff --git a/src/Modules/OtherModule.cs b/src/Modules/OtherModule.cs
index 9b84113..e316a7f 100644
--- a/src/Modules/OtherModule.cs
+++ b/src/Modules/OtherModule.cs
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Linq;
using System.Net.Http;
@@ -18,6 +19,7 @@
using Fergun.Interactive.Pagination;
using Fergun.Modules.Handlers;
using Fergun.Preconditions;
+using Fergun.Services;
using Humanizer;
using Humanizer.Localisation;
using Microsoft.EntityFrameworkCore;
@@ -39,9 +41,10 @@ public class OtherModule : InteractionModuleBase
private readonly IGeniusClient _geniusClient;
private readonly HttpClient _httpClient;
private readonly FergunContext _db;
+ private readonly ApplicationCommandCache _commandCache;
public OtherModule(ILogger logger, IFergunLocalizer localizer, IOptionsSnapshot fergunOptions,
- InteractiveService interactive, IGeniusClient geniusClient, HttpClient httpClient, FergunContext db)
+ InteractiveService interactive, IGeniusClient geniusClient, HttpClient httpClient, FergunContext db, ApplicationCommandCache commandCache)
{
_logger = logger;
_localizer = localizer;
@@ -50,6 +53,7 @@ public OtherModule(ILogger logger, IFergunLocalizer lo
_httpClient = httpClient;
_interactive = interactive;
_db = db;
+ _commandCache = commandCache;
}
public override void BeforeExecute(ICommandInfo command) => _localizer.CurrentCulture = CultureInfo.GetCultureInfo(Context.Interaction.GetLanguageCode());
@@ -147,6 +151,64 @@ public async Task LyricsAsync([MaxValue(int.MaxValue)] [Autocompl
{
await Context.Interaction.DeferAsync();
+ return await LyricsInternalAsync(id);
+ }
+
+ [Ratelimit(1, Constants.GlobalRatelimitPeriod)]
+ [SlashCommand("lyrics-spotify", "Gets the lyrics of the song you're listening to on Spotify.")]
+ public async Task LyricsSpotifyAsync()
+ {
+ var spotifyActivity = Context.User.Activities.OfType().FirstOrDefault();
+ if (spotifyActivity is null)
+ {
+ return FergunResult.FromError(_localizer["NoSpotifyActivity"], true);
+ }
+
+ await Context.Interaction.DeferAsync();
+
+ string artist = spotifyActivity.Artists.First();
+ string title = RemoveTitleExtraInfo(spotifyActivity.TrackTitle);
+ string query = $"{artist} {title}";
+
+ _logger.LogInformation("Detected Spotify activity on user {User} ({UserId})", Context.User, Context.User.Id);
+ _logger.LogInformation("Searching for songs matching Spotify song \"{Song}\"", $"{artist} - {title}");
+
+ var results = await _geniusClient.SearchSongsAsync(query);
+
+ var match = results.FirstOrDefault(x =>
+ x.PrimaryArtistNames.Equals(artist, StringComparison.InvariantCultureIgnoreCase) &&
+ x.Title.Equals(title, StringComparison.InvariantCultureIgnoreCase));
+
+ if (match is not null)
+ {
+ _logger.LogInformation("Found exact match for Spotify song \"{Song}\"", match);
+ if (match.IsInstrumental)
+ {
+ return FergunResult.FromError(_localizer["LyricsInstrumental", match]);
+ }
+
+ if (match.LyricsState == "unreleased")
+ {
+ return FergunResult.FromError(_localizer["LyricsUnreleased", match]);
+ }
+ }
+ else
+ {
+ match = results.FirstOrDefault(x => !x.IsInstrumental && x.LyricsState != "unreleased" &&
+ (x.PrimaryArtistNames.Equals(artist, StringComparison.InvariantCultureIgnoreCase) ||
+ x.Title.Equals(title, StringComparison.InvariantCultureIgnoreCase)));
+ }
+
+ if (match is null)
+ {
+ return FergunResult.FromError(_localizer["NoSongMatchFound", $"{artist} - {title}"]);
+ }
+
+ return await LyricsInternalAsync(match.Id, false);
+ }
+
+ private async Task LyricsInternalAsync(int id, bool checkSpotifyStatus = true)
+ {
_logger.LogInformation("Requesting song from Genius with ID {Id}", id);
var song = await _geniusClient.GetSongAsync(id);
@@ -175,6 +237,9 @@ public async Task LyricsAsync([MaxValue(int.MaxValue)] [Autocompl
return FergunResult.FromError(_localizer["LyricsEmpty", $"{song.ArtistNames} - {song.Title}"]);
}
+ var spotifyLyricsCommand = _commandCache.CachedCommands.FirstOrDefault(x => x.Name == "lyrics-spotify");
+ Debug.Assert(spotifyLyricsCommand != null, "Expected /lyrics-spotify to be present");
+
var chunks = song.Lyrics.SplitForPagination(EmbedBuilder.MaxDescriptionLength).ToArray();
_logger.LogDebug("Split lyrics into {Chunks}", "chunk".ToQuantity(chunks.Length));
@@ -201,13 +266,36 @@ PageBuilder GeneratePage(int index)
if (song.SpotifyTrackId is not null)
links += $" | {Format.Url(_localizer["OpenInSpotify"], $"https://open.spotify.com/track/{song.SpotifyTrackId}?go=1")}";
- return new PageBuilder()
+ var builder = new PageBuilder()
.WithTitle($"{song.ArtistNames} - {song.Title}".Truncate(EmbedBuilder.MaxTitleLength))
.WithThumbnailUrl(song.SongArtImageUrl)
.WithDescription(chunks[index].ToString())
- .AddField(_localizer["Links"], links)
.WithFooter(_localizer["GeniusPaginatorFooter", index + 1, chunks.Length], Constants.GeniusLogoUrl)
.WithColor(Color.Orange);
+
+ if (checkSpotifyStatus && IsSameSong())
+ {
+ var mention = $"{spotifyLyricsCommand.Name}:{spotifyLyricsCommand.Id}>";
+ builder.AddField(_localizer["Note"], _localizer["UseSpotifyLyricsCommand", mention]);
+ }
+
+ builder.AddField(_localizer["Links"], links);
+
+ return builder;
+ }
+
+ bool IsSameSong()
+ {
+ var spotifyActivity = Context.User.Activities.OfType().FirstOrDefault();
+ if (spotifyActivity is null)
+ return false;
+
+ string artist = spotifyActivity.Artists.First();
+ string title = RemoveTitleExtraInfo(spotifyActivity.TrackTitle);
+
+ return (song.SpotifyTrackId is not null && song.SpotifyTrackId == spotifyActivity.TrackId) ||
+ (song.PrimaryArtistNames.Equals(artist, StringComparison.InvariantCultureIgnoreCase) &&
+ song.Title.Equals(title, StringComparison.InvariantCultureIgnoreCase));
}
}
@@ -311,4 +399,14 @@ public async Task StatsAsync()
return FergunResult.FromSuccess();
}
+
+ [return: NotNullIfNotNull(nameof(input))]
+ private static string? RemoveTitleExtraInfo(string? input)
+ {
+ if (string.IsNullOrEmpty(input))
+ return input;
+
+ int index = input.IndexOf(" - ", StringComparison.Ordinal);
+ return index != -1 ? input[..index] : input;
+ }
}
\ No newline at end of file
diff --git a/src/Modules/UtilityModule.cs b/src/Modules/UtilityModule.cs
index 8da598b..3080328 100644
--- a/src/Modules/UtilityModule.cs
+++ b/src/Modules/UtilityModule.cs
@@ -9,7 +9,6 @@
using System.Threading.Tasks;
using Discord;
using Discord.Interactions;
-using Discord.Rest;
using Fergun.Apis.Dictionary;
using Fergun.Apis.Wikipedia;
using Fergun.Apis.WolframAlpha;
@@ -20,6 +19,7 @@
using Fergun.Interactive.Selection;
using Fergun.Modules.Handlers;
using Fergun.Preconditions;
+using Fergun.Services;
using GTranslate;
using GTranslate.Results;
using Humanizer;
@@ -41,7 +41,6 @@ namespace Fergun.Modules;
[Ratelimit(Constants.GlobalCommandUsesPerPeriod, Constants.GlobalRatelimitPeriod)]
public class UtilityModule : InteractionModuleBase
{
- private static IReadOnlyCollection? _cachedCommands;
private static readonly DrawingOptions _cachedDrawingOptions = new();
private static readonly PngEncoder _cachedPngEncoder = new() { CompressionLevel = PngCompressionLevel.BestCompression, SkipMetadata = true };
private static readonly Lazy _lazyFilteredLanguages = new(() => Language.LanguageDictionary
@@ -51,28 +50,28 @@ public class UtilityModule : InteractionModuleBase
private readonly ILogger _logger;
private readonly IFergunLocalizer _localizer;
- private readonly StartupOptions _startupOptions;
private readonly FergunOptions _fergunOptions;
private readonly SharedModule _shared;
private readonly InteractionService _commands;
private readonly InteractiveService _interactive;
+ private readonly ApplicationCommandCache _commandCache;
private readonly IFergunTranslator _translator;
private readonly IDictionaryClient _dictionary;
private readonly SearchClient _searchClient;
private readonly IWikipediaClient _wikipediaClient;
private readonly IWolframAlphaClient _wolframAlphaClient;
- public UtilityModule(ILogger logger, IFergunLocalizer localizer, IOptions startupOptions,
- IOptionsSnapshot fergunOptions, SharedModule shared, InteractionService commands, InteractiveService interactive,
- IDictionaryClient dictionary, IFergunTranslator translator, SearchClient searchClient, IWikipediaClient wikipediaClient, IWolframAlphaClient wolframAlphaClient)
+ public UtilityModule(ILogger logger, IFergunLocalizer localizer, IOptionsSnapshot fergunOptions,
+ SharedModule shared, InteractionService commands, InteractiveService interactive, ApplicationCommandCache commandCache, IDictionaryClient dictionary,
+ IFergunTranslator translator, SearchClient searchClient, IWikipediaClient wikipediaClient, IWolframAlphaClient wolframAlphaClient)
{
_logger = logger;
_localizer = localizer;
- _startupOptions = startupOptions.Value;
_fergunOptions = fergunOptions.Value;
_shared = shared;
_commands = commands;
_interactive = interactive;
+ _commandCache = commandCache;
_dictionary = dictionary;
_translator = translator;
_searchClient = searchClient;
@@ -366,25 +365,6 @@ public async Task DefineAsync(
[SlashCommand("help", "Information about Fergun.")]
public async Task HelpAsync()
{
- var responseType = InteractionResponseType.ChannelMessageWithSource;
-
- if (_cachedCommands is null)
- {
- responseType = InteractionResponseType.DeferredChannelMessageWithSource;
- await Context.Interaction.DeferAsync();
- }
-
- if (_startupOptions.TestingGuildId == 0)
- {
- _logger.LogDebug("Requesting global commands from REST API");
- _cachedCommands ??= await ((ShardedInteractionContext)Context).Client.Rest.GetGlobalApplicationCommands(true);
- }
- else
- {
- _logger.LogDebug("Requesting testing guild commands ({TestingGuildId}) from REST API", _startupOptions.TestingGuildId);
- _cachedCommands ??= await ((ShardedInteractionContext)Context).Client.Rest.GetGuildApplicationCommands(_startupOptions.TestingGuildId, true);
- }
-
string description = _localizer["Fergun2Info"];
var links = new List();
@@ -437,6 +417,7 @@ public async Task HelpAsync()
InteractiveMessageResult? result = null;
var interaction = Context.Interaction;
+ var responseType = InteractionResponseType.ChannelMessageWithSource;
_logger.LogInformation("Displaying help menu to user {User} ({Id})", Context.User, Context.User.Id);
@@ -464,7 +445,7 @@ public async Task HelpAsync()
string locale = Context.Interaction.UserLocale;
if (module.IsSlashGroup)
{
- var group = _cachedCommands
+ var group = _commandCache.CachedCommands
.First(globalCommand => module.SlashGroupName == globalCommand.Name);
// Slash command mentions can't be localized
@@ -474,7 +455,7 @@ public async Task HelpAsync()
}
else
{
- commandDescriptions = _cachedCommands
+ commandDescriptions = _commandCache.CachedCommands
.Where(globalCommand => module.SlashCommands.Any(slashCommand => globalCommand.Name == slashCommand.Name))
.OrderBy(x => x.NameLocalized ?? x.Name)
.Select(x => $"{x.Name}:{x.Id}> - {x.DescriptionLocalizations.GetValueOrDefault(locale, x.Description)}");
diff --git a/src/Program.cs b/src/Program.cs
index 815ea9f..2f8dda7 100644
--- a/src/Program.cs
+++ b/src/Program.cs
@@ -57,7 +57,7 @@
config.SocketConfig = new DiscordSocketConfig
{
LogLevel = LogSeverity.Verbose,
- GatewayIntents = GatewayIntents.Guilds,
+ GatewayIntents = GatewayIntents.Guilds | GatewayIntents.GuildPresences,
UseInteractionSnowflakeDate = false,
LogGatewayIntentWarnings = false,
FormatUsersInBidirectionalUnicode = false
@@ -74,6 +74,7 @@
builder.Services.AddLocalization(options => options.ResourcesPath = "Resources");
builder.Services.AddTransient(typeof(IFergunLocalizer<>), typeof(FergunLocalizer<>));
builder.Services.AddSingleton();
+builder.Services.AddSingleton();
builder.Services.AddHostedService();
builder.Services.AddHostedService();
builder.Services.ConfigureHttpClientDefaults(b =>
diff --git a/src/Resources/Modules.OtherModule.es.resx b/src/Resources/Modules.OtherModule.es.resx
index 7889407..e880429 100644
--- a/src/Resources/Modules.OtherModule.es.resx
+++ b/src/Resources/Modules.OtherModule.es.resx
@@ -123,12 +123,6 @@
Versión del bot
-
- Muestra las estadísticas de uso de los comandos.
-
-
- estadísticas-de-comandos
-
Estadísticas de Comandos
@@ -141,18 +135,6 @@
Letra por Genius | Página {0} de {1}
-
- Envía una cita inspiradora.
-
-
- inspirobot
-
-
- Invita Fergun a tu servidor.
-
-
- invitar
-
Haga click en el botón de abajo para invitar a Fergun a tu servidor.
@@ -165,18 +147,6 @@
Enlaces
-
- Obtiene la letra de una canción.
-
-
- letra
-
-
- El nombre de la canción.
-
-
- nombre
-
No es posible obtener la letra de "{0}".
@@ -213,12 +183,6 @@
ID de shard
-
- Envía las estadísticas del bot.
-
-
- estadísticas
-
Servidores Totales
@@ -237,4 +201,58 @@
Ver en Musixmatch
+
+ No se ha detectado la actividad de Spotify.
+
+
+ No es posible obtener la letra de la canción "{0}".
+
+
+ Obtiene la letra de la canción que estás escuchando en Spotify.
+
+
+ Muestra las estadísticas de uso de los comandos.
+
+
+ estadísticas-de-comandos
+
+
+ Envía una cita inspiradora.
+
+
+ inspirobot
+
+
+ Invita Fergun a tu servidor.
+
+
+ invitar
+
+
+ Obtiene la letra de una canción.
+
+
+ letra
+
+
+ El nombre de la canción.
+
+
+ nombre
+
+
+ Envía las estadísticas del bot.
+
+
+ estadísticas
+
+
+ letra-spotify
+
+
+ Nota
+
+
+ ℹ️ Usa {0} para obtener fácilmente la letra de la canción que estás escuchando.
+
\ No newline at end of file
diff --git a/src/Resources/Modules.OtherModule.resx b/src/Resources/Modules.OtherModule.resx
index 6b598ec..a607278 100644
--- a/src/Resources/Modules.OtherModule.resx
+++ b/src/Resources/Modules.OtherModule.resx
@@ -151,7 +151,7 @@
Unable to get the lyrics of "{0}".
- "{0}" is instrumental.
+ "{0}" is an instrumental song.
Unable to find a song with ID {0}. Use the autocomplete results.
@@ -201,4 +201,19 @@
View on Musixmatch
+
+ No Spotify activity detected.
+
+
+ Unable to get the lyrics of song "{0}".
+
+
+
+
+
+ Note
+
+
+ ℹ️ Use {0} to easily get the lyrics of the song you're currently listening to.
+
\ No newline at end of file
diff --git a/src/Services/ApplicationCommandCache.cs b/src/Services/ApplicationCommandCache.cs
new file mode 100644
index 0000000..f2b0df0
--- /dev/null
+++ b/src/Services/ApplicationCommandCache.cs
@@ -0,0 +1,15 @@
+using System.Collections.Generic;
+using Discord;
+
+namespace Fergun.Services;
+
+///
+/// Stores the registered application commands.
+///
+public class ApplicationCommandCache
+{
+ ///
+ /// Gets the registered application commands.
+ ///
+ public IReadOnlyCollection CachedCommands { get; internal set; } = [];
+}
\ No newline at end of file
diff --git a/src/Services/InteractionHandlingService.cs b/src/Services/InteractionHandlingService.cs
index 05a1265..f220fac 100644
--- a/src/Services/InteractionHandlingService.cs
+++ b/src/Services/InteractionHandlingService.cs
@@ -27,6 +27,7 @@ public sealed class InteractionHandlingService : IHostedService, IDisposable
{
private readonly DiscordShardedClient _shardedClient;
private readonly InteractionService _interactionService;
+ private readonly ApplicationCommandCache _commandCache;
private readonly FergunLocalizationManager _localizationManager;
private readonly ILogger _logger;
private readonly IServiceProvider _services;
@@ -35,11 +36,12 @@ public sealed class InteractionHandlingService : IHostedService, IDisposable
private readonly SemaphoreSlim _cmdStatsSemaphore = new(1, 1);
private bool _disposed;
- public InteractionHandlingService(DiscordShardedClient client, InteractionService interactionService, FergunLocalizationManager localizationManager,
- ILogger logger, IServiceProvider services, IOptions options)
+ public InteractionHandlingService(DiscordShardedClient client, InteractionService interactionService, ApplicationCommandCache commandCache,
+ FergunLocalizationManager localizationManager, ILogger logger, IServiceProvider services, IOptions options)
{
_shardedClient = client;
_interactionService = interactionService;
+ _commandCache = commandCache;
_localizationManager = localizationManager;
_logger = logger;
_services = services;
@@ -110,7 +112,7 @@ public async Task ReadyAsync()
if (_testingGuildId == 0)
{
_logger.LogInformation("Registering commands globally");
- await _interactionService.AddModulesGloballyAsync(true, modules.ToArray());
+ _commandCache.CachedCommands = await _interactionService.AddModulesGloballyAsync(true, modules.ToArray());
if (_ownerCommandsGuildId != 0)
{
@@ -124,11 +126,11 @@ public async Task ReadyAsync()
if (_testingGuildId == _ownerCommandsGuildId)
{
- await _interactionService.RegisterCommandsToGuildAsync(_testingGuildId);
+ _commandCache.CachedCommands = await _interactionService.RegisterCommandsToGuildAsync(_testingGuildId);
}
else
{
- await _interactionService.AddModulesToGuildAsync(_testingGuildId, true, modules.ToArray());
+ _commandCache.CachedCommands = await _interactionService.AddModulesToGuildAsync(_testingGuildId, true, modules.ToArray());
if (_ownerCommandsGuildId != 0)
{
diff --git a/tests/Fergun.Tests/Modules/UtilityModuleTests.cs b/tests/Fergun.Tests/Modules/UtilityModuleTests.cs
index 579906d..5d346a1 100644
--- a/tests/Fergun.Tests/Modules/UtilityModuleTests.cs
+++ b/tests/Fergun.Tests/Modules/UtilityModuleTests.cs
@@ -8,12 +8,11 @@
using Fergun.Apis.Dictionary;
using Fergun.Apis.Wikipedia;
using Fergun.Apis.WolframAlpha;
-using Fergun.Configuration;
using Fergun.Interactive;
using Fergun.Modules;
+using Fergun.Services;
using GTranslate.Translators;
using Microsoft.Extensions.Logging;
-using Microsoft.Extensions.Options;
using Moq;
using Xunit;
using YoutubeExplode.Search;
@@ -34,14 +33,14 @@ public class UtilityModuleTests
public UtilityModuleTests()
{
- var startupOptions = Mock.Of>();
+ var commandCache = new ApplicationCommandCache();
var options = Utils.CreateMockedFergunOptions();
var client = new DiscordSocketClient();
SharedModule shared = new(Mock.Of>(), Utils.CreateMockedLocalizer(), Mock.Of(), _googleTranslator2);
var interactionService = new InteractionService(client);
var interactive = new InteractiveService(client, new InteractiveConfig { ReturnAfterSendingPaginator = true });
- _moduleMock = new Mock(() => new UtilityModule(Mock.Of>(), _localizer, startupOptions, options, shared, interactionService,
- interactive, _dictionaryClient, Mock.Of(), _searchClient, _wikipediaClient, _wolframAlphaClient)) { CallBase = true };
+ _moduleMock = new Mock(() => new UtilityModule(Mock.Of>(), _localizer, options, shared, interactionService,
+ interactive, commandCache, _dictionaryClient, Mock.Of(), _searchClient, _wikipediaClient, _wolframAlphaClient)) { CallBase = true };
_contextMock.SetupGet(x => x.Interaction).Returns(_interactionMock.Object);
((IInteractionModuleBase)_moduleMock.Object).SetContext(_contextMock.Object);
}