added .lyrics command - not very good
This commit is contained in:
parent
ad472fd52e
commit
2efa3c5347
16 changed files with 420 additions and 57 deletions
src/EllieBot
|
@ -7,13 +7,24 @@ namespace EllieBot.Modules.Music;
|
||||||
[NoPublicBot]
|
[NoPublicBot]
|
||||||
public sealed partial class Music : EllieModule<IMusicService>
|
public sealed partial class Music : EllieModule<IMusicService>
|
||||||
{
|
{
|
||||||
public enum All { All = -1 }
|
public enum All
|
||||||
|
{
|
||||||
|
All = -1
|
||||||
|
}
|
||||||
|
|
||||||
public enum InputRepeatType
|
public enum InputRepeatType
|
||||||
{
|
{
|
||||||
N = 0, No = 0, None = 0,
|
N = 0,
|
||||||
T = 1, Track = 1, S = 1, Song = 1,
|
No = 0,
|
||||||
Q = 2, Queue = 2, Playlist = 2, Pl = 2
|
None = 0,
|
||||||
|
T = 1,
|
||||||
|
Track = 1,
|
||||||
|
S = 1,
|
||||||
|
Song = 1,
|
||||||
|
Q = 2,
|
||||||
|
Queue = 2,
|
||||||
|
Playlist = 2,
|
||||||
|
Pl = 2
|
||||||
}
|
}
|
||||||
|
|
||||||
public const string MUSIC_ICON_URL = "https://i.imgur.com/nhKS3PT.png";
|
public const string MUSIC_ICON_URL = "https://i.imgur.com/nhKS3PT.png";
|
||||||
|
@ -22,9 +33,13 @@ public sealed partial class Music : EllieModule<IMusicService>
|
||||||
|
|
||||||
private static readonly SemaphoreSlim _voiceChannelLock = new(1, 1);
|
private static readonly SemaphoreSlim _voiceChannelLock = new(1, 1);
|
||||||
private readonly ILogCommandService _logService;
|
private readonly ILogCommandService _logService;
|
||||||
|
private readonly ILyricsService _lyricsService;
|
||||||
|
|
||||||
public Music(ILogCommandService logService)
|
public Music(ILogCommandService logService, ILyricsService lyricsService)
|
||||||
=> _logService = logService;
|
{
|
||||||
|
_logService = logService;
|
||||||
|
_lyricsService = lyricsService;
|
||||||
|
}
|
||||||
|
|
||||||
private async Task<bool> ValidateAsync()
|
private async Task<bool> ValidateAsync()
|
||||||
{
|
{
|
||||||
|
@ -110,10 +125,10 @@ public sealed partial class Music : EllieModule<IMusicService>
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var embed = CreateEmbed()
|
var embed = CreateEmbed()
|
||||||
.WithOkColor()
|
.WithOkColor()
|
||||||
.WithAuthor(GetText(strs.queued_track) + " #" + (index + 1), MUSIC_ICON_URL)
|
.WithAuthor(GetText(strs.queued_track) + " #" + (index + 1), MUSIC_ICON_URL)
|
||||||
.WithDescription($"{trackInfo.PrettyName()}\n{GetText(strs.queue)} ")
|
.WithDescription($"{trackInfo.PrettyName()}\n{GetText(strs.queue)} ")
|
||||||
.WithFooter(trackInfo.Platform.ToString());
|
.WithFooter(trackInfo.Platform.ToString());
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(trackInfo.Thumbnail))
|
if (!string.IsNullOrWhiteSpace(trackInfo.Thumbnail))
|
||||||
embed.WithThumbnailUrl(trackInfo.Thumbnail);
|
embed.WithThumbnailUrl(trackInfo.Thumbnail);
|
||||||
|
@ -301,39 +316,39 @@ public sealed partial class Music : EllieModule<IMusicService>
|
||||||
|
|
||||||
|
|
||||||
desc += tracks
|
desc += tracks
|
||||||
.Select((v, index) =>
|
.Select((v, index) =>
|
||||||
{
|
{
|
||||||
index += LQ_ITEMS_PER_PAGE * curPage;
|
index += LQ_ITEMS_PER_PAGE * curPage;
|
||||||
if (index == currentIndex)
|
if (index == currentIndex)
|
||||||
return $"**⇒**`{index + 1}.` {v.PrettyFullName()}";
|
return $"**⇒**`{index + 1}.` {v.PrettyFullName()}";
|
||||||
|
|
||||||
return $"`{index + 1}.` {v.PrettyFullName()}";
|
return $"`{index + 1}.` {v.PrettyFullName()}";
|
||||||
})
|
})
|
||||||
.Join('\n');
|
.Join('\n');
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(add))
|
if (!string.IsNullOrWhiteSpace(add))
|
||||||
desc = add + "\n" + desc;
|
desc = add + "\n" + desc;
|
||||||
|
|
||||||
var embed = CreateEmbed()
|
var embed = CreateEmbed()
|
||||||
.WithAuthor(
|
.WithAuthor(
|
||||||
GetText(strs.player_queue(curPage + 1, (tracks.Count / LQ_ITEMS_PER_PAGE) + 1)),
|
GetText(strs.player_queue(curPage + 1, (tracks.Count / LQ_ITEMS_PER_PAGE) + 1)),
|
||||||
MUSIC_ICON_URL)
|
MUSIC_ICON_URL)
|
||||||
.WithDescription(desc)
|
.WithDescription(desc)
|
||||||
.WithFooter(
|
.WithFooter(
|
||||||
$" {mp.PrettyVolume()} | 🎶 {tracks.Count} | ⌛ {mp.PrettyTotalTime()} ")
|
$" {mp.PrettyVolume()} | 🎶 {tracks.Count} | ⌛ {mp.PrettyTotalTime()} ")
|
||||||
.WithOkColor();
|
.WithOkColor();
|
||||||
|
|
||||||
return embed;
|
return embed;
|
||||||
}
|
}
|
||||||
|
|
||||||
await Response()
|
await Response()
|
||||||
.Paginated()
|
.Paginated()
|
||||||
.Items(tracks)
|
.Items(tracks)
|
||||||
.PageSize(LQ_ITEMS_PER_PAGE)
|
.PageSize(LQ_ITEMS_PER_PAGE)
|
||||||
.CurrentPage(page)
|
.CurrentPage(page)
|
||||||
.AddFooter(false)
|
.AddFooter(false)
|
||||||
.Page(PrintAction)
|
.Page(PrintAction)
|
||||||
.SendAsync();
|
.SendAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
// search
|
// search
|
||||||
|
@ -353,15 +368,15 @@ public sealed partial class Music : EllieModule<IMusicService>
|
||||||
|
|
||||||
|
|
||||||
var embeds = videos.Select((x, i) => CreateEmbed()
|
var embeds = videos.Select((x, i) => CreateEmbed()
|
||||||
.WithOkColor()
|
.WithOkColor()
|
||||||
.WithThumbnailUrl(x.Thumbnail)
|
.WithThumbnailUrl(x.Thumbnail)
|
||||||
.WithDescription($"`{i + 1}.` {Format.Bold(x.Title)}\n\t{x.Url}"))
|
.WithDescription($"`{i + 1}.` {Format.Bold(x.Title)}\n\t{x.Url}"))
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
var msg = await Response()
|
var msg = await Response()
|
||||||
.Text(strs.queue_search_results)
|
.Text(strs.queue_search_results)
|
||||||
.Embeds(embeds)
|
.Embeds(embeds)
|
||||||
.SendAsync();
|
.SendAsync();
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
@ -425,10 +440,10 @@ public sealed partial class Music : EllieModule<IMusicService>
|
||||||
}
|
}
|
||||||
|
|
||||||
var embed = CreateEmbed()
|
var embed = CreateEmbed()
|
||||||
.WithAuthor(GetText(strs.removed_track) + " #" + index, MUSIC_ICON_URL)
|
.WithAuthor(GetText(strs.removed_track) + " #" + index, MUSIC_ICON_URL)
|
||||||
.WithDescription(track.PrettyName())
|
.WithDescription(track.PrettyName())
|
||||||
.WithFooter(track.PrettyInfo())
|
.WithFooter(track.PrettyInfo())
|
||||||
.WithErrorColor();
|
.WithErrorColor();
|
||||||
|
|
||||||
await _service.SendToOutputAsync(ctx.Guild.Id, embed);
|
await _service.SendToOutputAsync(ctx.Guild.Id, embed);
|
||||||
}
|
}
|
||||||
|
@ -593,11 +608,11 @@ public sealed partial class Music : EllieModule<IMusicService>
|
||||||
}
|
}
|
||||||
|
|
||||||
var embed = CreateEmbed()
|
var embed = CreateEmbed()
|
||||||
.WithTitle(track.Title.TrimTo(65))
|
.WithTitle(track.Title.TrimTo(65))
|
||||||
.WithAuthor(GetText(strs.track_moved), MUSIC_ICON_URL)
|
.WithAuthor(GetText(strs.track_moved), MUSIC_ICON_URL)
|
||||||
.AddField(GetText(strs.from_position), $"#{from + 1}", true)
|
.AddField(GetText(strs.from_position), $"#{from + 1}", true)
|
||||||
.AddField(GetText(strs.to_position), $"#{to + 1}", true)
|
.AddField(GetText(strs.to_position), $"#{to + 1}", true)
|
||||||
.WithOkColor();
|
.WithOkColor();
|
||||||
|
|
||||||
if (Uri.IsWellFormedUriString(track.Url, UriKind.Absolute))
|
if (Uri.IsWellFormedUriString(track.Url, UriKind.Absolute))
|
||||||
embed.WithUrl(track.Url);
|
embed.WithUrl(track.Url);
|
||||||
|
@ -652,12 +667,12 @@ public sealed partial class Music : EllieModule<IMusicService>
|
||||||
return;
|
return;
|
||||||
|
|
||||||
var embed = CreateEmbed()
|
var embed = CreateEmbed()
|
||||||
.WithOkColor()
|
.WithOkColor()
|
||||||
.WithAuthor(GetText(strs.now_playing), MUSIC_ICON_URL)
|
.WithAuthor(GetText(strs.now_playing), MUSIC_ICON_URL)
|
||||||
.WithDescription(currentTrack.PrettyName())
|
.WithDescription(currentTrack.PrettyName())
|
||||||
.WithThumbnailUrl(currentTrack.Thumbnail)
|
.WithThumbnailUrl(currentTrack.Thumbnail)
|
||||||
.WithFooter(
|
.WithFooter(
|
||||||
$"{mp.PrettyVolume()} | {mp.PrettyTotalTime()} | {currentTrack.Platform} | {currentTrack.Queuer}");
|
$"{mp.PrettyVolume()} | {mp.PrettyTotalTime()} | {currentTrack.Platform} | {currentTrack.Queuer}");
|
||||||
|
|
||||||
await Response().Embed(embed).SendAsync();
|
await Response().Embed(embed).SendAsync();
|
||||||
}
|
}
|
||||||
|
@ -768,4 +783,71 @@ public sealed partial class Music : EllieModule<IMusicService>
|
||||||
await Response().Confirm(strs.wrongsong_success(removed.Title.TrimTo(30))).SendAsync();
|
await Response().Confirm(strs.wrongsong_success(removed.Title.TrimTo(30))).SendAsync();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Cmd]
|
||||||
|
[RequireContext(ContextType.Guild)]
|
||||||
|
public async Task Lyrics([Leftover] string name = null)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(name))
|
||||||
|
{
|
||||||
|
if (_service.TryGetMusicPlayer(ctx.Guild.Id, out var mp)
|
||||||
|
&& mp.GetCurrentTrack(out _) is { } currentTrack)
|
||||||
|
{
|
||||||
|
name = currentTrack.Title;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var tracks = await _lyricsService.SearchTracksAsync(name);
|
||||||
|
|
||||||
|
if (tracks.Count == 0)
|
||||||
|
{
|
||||||
|
await Response().Error(strs.no_lyrics_found).SendAsync();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var embed = CreateEmbed()
|
||||||
|
.WithFooter("type 1-5 to select");
|
||||||
|
|
||||||
|
for (var i = 0; i <= 5 && i < tracks.Count; i++)
|
||||||
|
{
|
||||||
|
var item = tracks[i];
|
||||||
|
embed.AddField($"`{(i + 1)}`. {item.Author}", item.Title, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
await Response()
|
||||||
|
.Embed(embed)
|
||||||
|
.SendAsync();
|
||||||
|
|
||||||
|
var input = await GetUserInputAsync(ctx.User.Id, ctx.Channel.Id, str => int.TryParse(str, out _));
|
||||||
|
|
||||||
|
if (input is null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var index = int.Parse(input) - 1;
|
||||||
|
if (index < 0 || index > 4)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var track = tracks[index];
|
||||||
|
var lyrics = await _lyricsService.GetLyricsAsync(track.Id);
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(lyrics))
|
||||||
|
{
|
||||||
|
await Response().Error(strs.no_lyrics_found).SendAsync();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await Response()
|
||||||
|
.Embed(CreateEmbed()
|
||||||
|
.WithOkColor()
|
||||||
|
.WithAuthor(track.Author)
|
||||||
|
.WithTitle(track.Title)
|
||||||
|
.WithDescription(lyrics))
|
||||||
|
.SendAsync();
|
||||||
|
}
|
||||||
}
|
}
|
7
src/EllieBot/Modules/Music/Services/ILyricsService.cs
Normal file
7
src/EllieBot/Modules/Music/Services/ILyricsService.cs
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
namespace EllieBot.Modules.Music;
|
||||||
|
|
||||||
|
public interface ILyricsService
|
||||||
|
{
|
||||||
|
public Task<IReadOnlyList<TracksItem>> SearchTracksAsync(string name);
|
||||||
|
public Task<string> GetLyricsAsync(int trackId);
|
||||||
|
}
|
25
src/EllieBot/Modules/Music/Services/LyricsService.cs
Normal file
25
src/EllieBot/Modules/Music/Services/LyricsService.cs
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
using Musix;
|
||||||
|
|
||||||
|
namespace EllieBot.Modules.Music;
|
||||||
|
|
||||||
|
public sealed class LyricsService(HttpClient client) : ILyricsService, IEService
|
||||||
|
{
|
||||||
|
private readonly MusixMatchAPI _api = new(client);
|
||||||
|
|
||||||
|
private static string NormalizeName(string name)
|
||||||
|
=> string.Join("-", name.Split()
|
||||||
|
.Select(x => new string(x.Where(c => char.IsLetterOrDigit(c)).ToArray())))
|
||||||
|
.Trim('-');
|
||||||
|
|
||||||
|
public async Task<IReadOnlyList<TracksItem>> SearchTracksAsync(string name)
|
||||||
|
=> await _api.SearchTracksAsync(NormalizeName(name))
|
||||||
|
.Fmap(x => x
|
||||||
|
.Message
|
||||||
|
.Body
|
||||||
|
.TrackList
|
||||||
|
.Map(x => new TracksItem(x.Track.ArtistName, x.Track.TrackName, x.Track.TrackId)));
|
||||||
|
|
||||||
|
public async Task<string> GetLyricsAsync(int trackId)
|
||||||
|
=> await _api.GetTrackLyricsAsync(trackId)
|
||||||
|
.Fmap(x => x.Message.Body.Lyrics.LyricsBody);
|
||||||
|
}
|
12
src/EllieBot/Modules/Music/_common/Musix/Header.cs
Normal file
12
src/EllieBot/Modules/Music/_common/Musix/Header.cs
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace Musix.Models;
|
||||||
|
|
||||||
|
public class Header
|
||||||
|
{
|
||||||
|
[JsonPropertyName("status_code")]
|
||||||
|
public int StatusCode { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("execute_time")]
|
||||||
|
public double ExecuteTime { get; set; }
|
||||||
|
}
|
9
src/EllieBot/Modules/Music/_common/Musix/Lyrics.cs
Normal file
9
src/EllieBot/Modules/Music/_common/Musix/Lyrics.cs
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace Musix.Models;
|
||||||
|
|
||||||
|
public class Lyrics
|
||||||
|
{
|
||||||
|
[JsonPropertyName("lyrics_body")]
|
||||||
|
public string LyricsBody { get; set; } = string.Empty;
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace Musix.Models;
|
||||||
|
|
||||||
|
public class LyricsResponse
|
||||||
|
{
|
||||||
|
[JsonPropertyName("lyrics")]
|
||||||
|
public Lyrics Lyrics { get; set; } = null!;
|
||||||
|
}
|
12
src/EllieBot/Modules/Music/_common/Musix/Message.cs
Normal file
12
src/EllieBot/Modules/Music/_common/Musix/Message.cs
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace Musix.Models;
|
||||||
|
|
||||||
|
public class Message<T>
|
||||||
|
{
|
||||||
|
[JsonPropertyName("header")]
|
||||||
|
public Header Header { get; set; } = null!;
|
||||||
|
|
||||||
|
[JsonPropertyName("body")]
|
||||||
|
public T Body { get; set; } = default!;
|
||||||
|
}
|
141
src/EllieBot/Modules/Music/_common/Musix/MusixMatchAPI.cs
Normal file
141
src/EllieBot/Modules/Music/_common/Musix/MusixMatchAPI.cs
Normal file
|
@ -0,0 +1,141 @@
|
||||||
|
using System.Security.Cryptography;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Web;
|
||||||
|
using Microsoft.Extensions.Caching.Memory;
|
||||||
|
using Musix.Models;
|
||||||
|
|
||||||
|
// All credit goes to https://github.com/Strvm/musicxmatch-api for the original implementation
|
||||||
|
namespace Musix
|
||||||
|
{
|
||||||
|
public sealed class MusixMatchAPI
|
||||||
|
{
|
||||||
|
private readonly HttpClient _httpClient;
|
||||||
|
private readonly string _baseUrl = "https://www.musixmatch.com/ws/1.1/";
|
||||||
|
|
||||||
|
private readonly string _userAgent =
|
||||||
|
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.0.0 Safari/537.36";
|
||||||
|
|
||||||
|
private readonly IMemoryCache _cache;
|
||||||
|
private readonly JsonSerializerOptions _jsonOptions;
|
||||||
|
|
||||||
|
public MusixMatchAPI(HttpClient httpClient)
|
||||||
|
{
|
||||||
|
_httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
|
||||||
|
_httpClient.DefaultRequestHeaders.UserAgent.ParseAdd(_userAgent);
|
||||||
|
_httpClient.DefaultRequestHeaders.Add("Cookie", "mxm_bab=AB");
|
||||||
|
|
||||||
|
_jsonOptions = new()
|
||||||
|
{
|
||||||
|
PropertyNameCaseInsensitive = true
|
||||||
|
};
|
||||||
|
_cache = new MemoryCache(new MemoryCacheOptions { });
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<string> GetLatestAppUrlAsync()
|
||||||
|
{
|
||||||
|
var url = "https://www.musixmatch.com/search";
|
||||||
|
using var request = new HttpRequestMessage(HttpMethod.Get, url);
|
||||||
|
request.Headers.UserAgent.ParseAdd(
|
||||||
|
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36");
|
||||||
|
request.Headers.Add("Cookie", "mxm_bab=AB");
|
||||||
|
|
||||||
|
var response = await _httpClient.SendAsync(request);
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
|
var htmlContent = await response.Content.ReadAsStringAsync();
|
||||||
|
|
||||||
|
var pattern = @"src=""([^""]*/_next/static/chunks/pages/_app-[^""]+\.js)""";
|
||||||
|
var matches = Regex.Matches(htmlContent, pattern);
|
||||||
|
|
||||||
|
return matches.Count > 0
|
||||||
|
? matches[^1].Groups[1].Value
|
||||||
|
: throw new("_app URL not found in the HTML content.");
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<string> GetSecret()
|
||||||
|
{
|
||||||
|
var latestAppUrl = await GetLatestAppUrlAsync();
|
||||||
|
var response = await _httpClient.GetAsync(latestAppUrl);
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
|
var javascriptCode = await response.Content.ReadAsStringAsync();
|
||||||
|
|
||||||
|
var pattern = @"from\(\s*""(.*?)""\s*\.split";
|
||||||
|
var match = Regex.Match(javascriptCode, pattern);
|
||||||
|
|
||||||
|
if (match.Success)
|
||||||
|
{
|
||||||
|
var encodedString = match.Groups[1].Value;
|
||||||
|
var reversedString = new string(encodedString.Reverse().ToArray());
|
||||||
|
var decodedBytes = Convert.FromBase64String(reversedString);
|
||||||
|
return Encoding.UTF8.GetString(decodedBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Exception("Encoded string not found in the JavaScript code.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// It seems this is required in order to have multiword queries.
|
||||||
|
// Spaces don't work in the original implementation either
|
||||||
|
private string UrlEncode(string value)
|
||||||
|
=> HttpUtility.UrlEncode(value)
|
||||||
|
.Replace("+", "-");
|
||||||
|
|
||||||
|
private async Task<string> GenerateSignature(string url)
|
||||||
|
{
|
||||||
|
var currentDate = DateTime.Now;
|
||||||
|
var l = currentDate.Year.ToString();
|
||||||
|
var s = currentDate.Month.ToString("D2");
|
||||||
|
var r = currentDate.Day.ToString("D2");
|
||||||
|
|
||||||
|
var message = (url + l + s + r);
|
||||||
|
var secret = await _cache.GetOrCreateAsync("secret", async _ => await GetSecret());
|
||||||
|
var key = Encoding.UTF8.GetBytes(secret ?? string.Empty);
|
||||||
|
var messageBytes = Encoding.UTF8.GetBytes(message);
|
||||||
|
|
||||||
|
using var hmac = new HMACSHA256(key);
|
||||||
|
var hashBytes = hmac.ComputeHash(messageBytes);
|
||||||
|
var signature = Convert.ToBase64String(hashBytes);
|
||||||
|
return $"&signature={UrlEncode(signature)}&signature_protocol=sha256";
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<MusixMatchResponse<TrackSearchResponse>> SearchTracksAsync(string trackQuery, int page = 1)
|
||||||
|
{
|
||||||
|
var endpoint =
|
||||||
|
$"track.search?app_id=community-app-v1.0&format=json&q={UrlEncode(trackQuery)}&f_has_lyrics=true&page_size=100&page={page}";
|
||||||
|
var jsonResponse = await MakeRequestAsync(endpoint);
|
||||||
|
return JsonSerializer.Deserialize<MusixMatchResponse<TrackSearchResponse>>(jsonResponse, _jsonOptions)
|
||||||
|
?? throw new JsonException("Failed to deserialize track search response");
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<MusixMatchResponse<LyricsResponse>> GetTrackLyricsAsync(int trackId)
|
||||||
|
{
|
||||||
|
var endpoint = $"track.lyrics.get?app_id=community-app-v1.0&format=json&track_id={trackId}";
|
||||||
|
var jsonResponse = await MakeRequestAsync(endpoint);
|
||||||
|
return JsonSerializer.Deserialize<MusixMatchResponse<LyricsResponse>>(jsonResponse, _jsonOptions)
|
||||||
|
?? throw new JsonException("Failed to deserialize lyrics response");
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<string> MakeRequestAsync(string endpoint)
|
||||||
|
{
|
||||||
|
var fullUrl = _baseUrl + endpoint;
|
||||||
|
var signedUrl = fullUrl + await GenerateSignature(fullUrl);
|
||||||
|
|
||||||
|
Console.WriteLine($"DEBUG - Request URL: {signedUrl}");
|
||||||
|
|
||||||
|
var request = new HttpRequestMessage(HttpMethod.Get, signedUrl);
|
||||||
|
request.Headers.UserAgent.ParseAdd(_userAgent);
|
||||||
|
request.Headers.Add("Cookie", "mxm_bab=AB");
|
||||||
|
|
||||||
|
var response = await _httpClient.SendAsync(request);
|
||||||
|
|
||||||
|
var content = await response.Content.ReadAsStringAsync();
|
||||||
|
if (!response.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"ERROR - Status: {response.StatusCode}, Content: {content}");
|
||||||
|
response.EnsureSuccessStatusCode(); // This will throw with the appropriate status code
|
||||||
|
}
|
||||||
|
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,11 @@
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace Musix.Models
|
||||||
|
{
|
||||||
|
public class MusixMatchResponse<T>
|
||||||
|
{
|
||||||
|
[JsonPropertyName("message")]
|
||||||
|
public Message<T> Message { get; set; } = null!;
|
||||||
|
}
|
||||||
|
}
|
23
src/EllieBot/Modules/Music/_common/Musix/Track.cs
Normal file
23
src/EllieBot/Modules/Music/_common/Musix/Track.cs
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace Musix.Models;
|
||||||
|
|
||||||
|
public class Track
|
||||||
|
{
|
||||||
|
[JsonPropertyName("track_id")]
|
||||||
|
public int TrackId { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("track_name")]
|
||||||
|
public string TrackName { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[JsonPropertyName("artist_name")]
|
||||||
|
public string ArtistName { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[JsonPropertyName("album_name")]
|
||||||
|
public string AlbumName { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[JsonPropertyName("track_share_url")]
|
||||||
|
public string TrackShareUrl { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public override string ToString() => $"{TrackName} by {ArtistName} (Album: {AlbumName})";
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace Musix.Models;
|
||||||
|
|
||||||
|
public class TrackListItem
|
||||||
|
{
|
||||||
|
[JsonPropertyName("track")]
|
||||||
|
public Track Track { get; set; } = null!;
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace Musix.Models;
|
||||||
|
|
||||||
|
public class TrackSearchResponse
|
||||||
|
{
|
||||||
|
[JsonPropertyName("track_list")]
|
||||||
|
public List<TrackListItem> TrackList { get; set; } = new();
|
||||||
|
}
|
3
src/EllieBot/Modules/Music/_common/TracksItem.cs
Normal file
3
src/EllieBot/Modules/Music/_common/TracksItem.cs
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
namespace EllieBot.Modules.Music;
|
||||||
|
|
||||||
|
public record struct TracksItem(string Author, string Title, int Id);
|
|
@ -1586,4 +1586,6 @@ fishspot:
|
||||||
xprate:
|
xprate:
|
||||||
- xprate
|
- xprate
|
||||||
xpratereset:
|
xpratereset:
|
||||||
- xpratereset
|
- xpratereset
|
||||||
|
lyrics:
|
||||||
|
- lyrics
|
|
@ -4977,4 +4977,12 @@ xpratereset:
|
||||||
params:
|
params:
|
||||||
- { }
|
- { }
|
||||||
- channel:
|
- channel:
|
||||||
desc: "The channel to reset the rate for."
|
desc: "The channel to reset the rate for."
|
||||||
|
lyrics:
|
||||||
|
desc: |-
|
||||||
|
Looks up lyrics for a song. Very hit or miss.
|
||||||
|
ex:
|
||||||
|
- 'biri biri'
|
||||||
|
params:
|
||||||
|
- song:
|
||||||
|
desc: "The song to look up lyrics for."
|
|
@ -1178,5 +1178,6 @@
|
||||||
"xp_rate_channel_set": "Channel **{0}** xp rate set to **{1}** xp per every **{2}** min.",
|
"xp_rate_channel_set": "Channel **{0}** xp rate set to **{1}** xp per every **{2}** min.",
|
||||||
"xp_rate_server_reset": "Server xp rate has been reset to global defaults.",
|
"xp_rate_server_reset": "Server xp rate has been reset to global defaults.",
|
||||||
"xp_rate_channel_reset": "Channel {0} xp rate has been reset.",
|
"xp_rate_channel_reset": "Channel {0} xp rate has been reset.",
|
||||||
"xp_rate_no_gain": "No xp gain"
|
"xp_rate_no_gain": "No xp gain",
|
||||||
|
"no_lyrics_found": "No lyrics found."
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue