Skip to content

Commit

Permalink
Add /lyrics-spotify command and cache registered app commands
Browse files Browse the repository at this point in the history
  • Loading branch information
d4n3436 committed Jun 28, 2024
1 parent bee7886 commit dc9b268
Show file tree
Hide file tree
Showing 10 changed files with 214 additions and 79 deletions.
1 change: 1 addition & 0 deletions src/Apis/Genius/GeniusSong.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ namespace Fergun.Apis.Genius;
/// <inheritdoc cref="IGeniusSong"/>
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,
Expand Down
5 changes: 5 additions & 0 deletions src/Apis/Genius/IGeniusSong.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@ public interface IGeniusSong
/// Gets the artist names.
/// </summary>
string ArtistNames { get; }

/// <summary>
/// Gets the primary artist names.
/// </summary>
string PrimaryArtistNames { get; }

/// <summary>
/// Gets the ID of this song.
Expand Down
104 changes: 101 additions & 3 deletions src/Modules/OtherModule.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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;
Expand All @@ -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<OtherModule> logger, IFergunLocalizer<OtherModule> localizer, IOptionsSnapshot<FergunOptions> fergunOptions,
InteractiveService interactive, IGeniusClient geniusClient, HttpClient httpClient, FergunContext db)
InteractiveService interactive, IGeniusClient geniusClient, HttpClient httpClient, FergunContext db, ApplicationCommandCache commandCache)
{
_logger = logger;
_localizer = localizer;
Expand All @@ -50,6 +53,7 @@ public OtherModule(ILogger<OtherModule> logger, IFergunLocalizer<OtherModule> lo
_httpClient = httpClient;
_interactive = interactive;
_db = db;
_commandCache = commandCache;
}

public override void BeforeExecute(ICommandInfo command) => _localizer.CurrentCulture = CultureInfo.GetCultureInfo(Context.Interaction.GetLanguageCode());
Expand Down Expand Up @@ -147,6 +151,64 @@ public async Task<RuntimeResult> 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<RuntimeResult> LyricsSpotifyAsync()
{
var spotifyActivity = Context.User.Activities.OfType<SpotifyGame>().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<RuntimeResult> LyricsInternalAsync(int id, bool checkSpotifyStatus = true)
{
_logger.LogInformation("Requesting song from Genius with ID {Id}", id);
var song = await _geniusClient.GetSongAsync(id);

Expand Down Expand Up @@ -175,6 +237,9 @@ public async Task<RuntimeResult> 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));

Expand All @@ -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<SpotifyGame>().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));
}
}

Expand Down Expand Up @@ -311,4 +399,14 @@ public async Task<RuntimeResult> 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;
}
}
37 changes: 9 additions & 28 deletions src/Modules/UtilityModule.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -41,7 +41,6 @@ namespace Fergun.Modules;
[Ratelimit(Constants.GlobalCommandUsesPerPeriod, Constants.GlobalRatelimitPeriod)]
public class UtilityModule : InteractionModuleBase
{
private static IReadOnlyCollection<RestApplicationCommand>? _cachedCommands;
private static readonly DrawingOptions _cachedDrawingOptions = new();
private static readonly PngEncoder _cachedPngEncoder = new() { CompressionLevel = PngCompressionLevel.BestCompression, SkipMetadata = true };
private static readonly Lazy<Language[]> _lazyFilteredLanguages = new(() => Language.LanguageDictionary
Expand All @@ -51,28 +50,28 @@ public class UtilityModule : InteractionModuleBase

private readonly ILogger<UtilityModule> _logger;
private readonly IFergunLocalizer<UtilityModule> _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<UtilityModule> logger, IFergunLocalizer<UtilityModule> localizer, IOptions<StartupOptions> startupOptions,
IOptionsSnapshot<FergunOptions> fergunOptions, SharedModule shared, InteractionService commands, InteractiveService interactive,
IDictionaryClient dictionary, IFergunTranslator translator, SearchClient searchClient, IWikipediaClient wikipediaClient, IWolframAlphaClient wolframAlphaClient)
public UtilityModule(ILogger<UtilityModule> logger, IFergunLocalizer<UtilityModule> localizer, IOptionsSnapshot<FergunOptions> 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;
Expand Down Expand Up @@ -366,25 +365,6 @@ public async Task<RuntimeResult> DefineAsync(
[SlashCommand("help", "Information about Fergun.")]
public async Task<RuntimeResult> 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<string>();
Expand Down Expand Up @@ -437,6 +417,7 @@ public async Task<RuntimeResult> HelpAsync()

InteractiveMessageResult<ModuleOption?>? result = null;
var interaction = Context.Interaction;
var responseType = InteractionResponseType.ChannelMessageWithSource;

_logger.LogInformation("Displaying help menu to user {User} ({Id})", Context.User, Context.User.Id);

Expand Down Expand Up @@ -464,7 +445,7 @@ public async Task<RuntimeResult> 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
Expand All @@ -474,7 +455,7 @@ public async Task<RuntimeResult> 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)}");
Expand Down
3 changes: 2 additions & 1 deletion src/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -74,6 +74,7 @@
builder.Services.AddLocalization(options => options.ResourcesPath = "Resources");
builder.Services.AddTransient(typeof(IFergunLocalizer<>), typeof(FergunLocalizer<>));
builder.Services.AddSingleton<FergunLocalizationManager>();
builder.Services.AddSingleton<ApplicationCommandCache>();
builder.Services.AddHostedService<InteractionHandlingService>();
builder.Services.AddHostedService<BotListService>();
builder.Services.ConfigureHttpClientDefaults(b =>
Expand Down
Loading

0 comments on commit dc9b268

Please sign in to comment.