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]
|
||||
public sealed partial class Music : EllieModule<IMusicService>
|
||||
{
|
||||
public enum All { All = -1 }
|
||||
public enum All
|
||||
{
|
||||
All = -1
|
||||
}
|
||||
|
||||
public enum InputRepeatType
|
||||
{
|
||||
N = 0, No = 0, None = 0,
|
||||
T = 1, Track = 1, S = 1, Song = 1,
|
||||
Q = 2, Queue = 2, Playlist = 2, Pl = 2
|
||||
N = 0,
|
||||
No = 0,
|
||||
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";
|
||||
|
@ -22,9 +33,13 @@ public sealed partial class Music : EllieModule<IMusicService>
|
|||
|
||||
private static readonly SemaphoreSlim _voiceChannelLock = new(1, 1);
|
||||
private readonly ILogCommandService _logService;
|
||||
private readonly ILyricsService _lyricsService;
|
||||
|
||||
public Music(ILogCommandService logService)
|
||||
=> _logService = logService;
|
||||
public Music(ILogCommandService logService, ILyricsService lyricsService)
|
||||
{
|
||||
_logService = logService;
|
||||
_lyricsService = lyricsService;
|
||||
}
|
||||
|
||||
private async Task<bool> ValidateAsync()
|
||||
{
|
||||
|
@ -110,10 +125,10 @@ public sealed partial class Music : EllieModule<IMusicService>
|
|||
try
|
||||
{
|
||||
var embed = CreateEmbed()
|
||||
.WithOkColor()
|
||||
.WithAuthor(GetText(strs.queued_track) + " #" + (index + 1), MUSIC_ICON_URL)
|
||||
.WithDescription($"{trackInfo.PrettyName()}\n{GetText(strs.queue)} ")
|
||||
.WithFooter(trackInfo.Platform.ToString());
|
||||
.WithOkColor()
|
||||
.WithAuthor(GetText(strs.queued_track) + " #" + (index + 1), MUSIC_ICON_URL)
|
||||
.WithDescription($"{trackInfo.PrettyName()}\n{GetText(strs.queue)} ")
|
||||
.WithFooter(trackInfo.Platform.ToString());
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(trackInfo.Thumbnail))
|
||||
embed.WithThumbnailUrl(trackInfo.Thumbnail);
|
||||
|
@ -301,39 +316,39 @@ public sealed partial class Music : EllieModule<IMusicService>
|
|||
|
||||
|
||||
desc += tracks
|
||||
.Select((v, index) =>
|
||||
{
|
||||
index += LQ_ITEMS_PER_PAGE * curPage;
|
||||
if (index == currentIndex)
|
||||
return $"**⇒**`{index + 1}.` {v.PrettyFullName()}";
|
||||
.Select((v, index) =>
|
||||
{
|
||||
index += LQ_ITEMS_PER_PAGE * curPage;
|
||||
if (index == currentIndex)
|
||||
return $"**⇒**`{index + 1}.` {v.PrettyFullName()}";
|
||||
|
||||
return $"`{index + 1}.` {v.PrettyFullName()}";
|
||||
})
|
||||
.Join('\n');
|
||||
return $"`{index + 1}.` {v.PrettyFullName()}";
|
||||
})
|
||||
.Join('\n');
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(add))
|
||||
desc = add + "\n" + desc;
|
||||
|
||||
var embed = CreateEmbed()
|
||||
.WithAuthor(
|
||||
GetText(strs.player_queue(curPage + 1, (tracks.Count / LQ_ITEMS_PER_PAGE) + 1)),
|
||||
MUSIC_ICON_URL)
|
||||
.WithDescription(desc)
|
||||
.WithFooter(
|
||||
$" {mp.PrettyVolume()} | 🎶 {tracks.Count} | ⌛ {mp.PrettyTotalTime()} ")
|
||||
.WithOkColor();
|
||||
.WithAuthor(
|
||||
GetText(strs.player_queue(curPage + 1, (tracks.Count / LQ_ITEMS_PER_PAGE) + 1)),
|
||||
MUSIC_ICON_URL)
|
||||
.WithDescription(desc)
|
||||
.WithFooter(
|
||||
$" {mp.PrettyVolume()} | 🎶 {tracks.Count} | ⌛ {mp.PrettyTotalTime()} ")
|
||||
.WithOkColor();
|
||||
|
||||
return embed;
|
||||
}
|
||||
|
||||
await Response()
|
||||
.Paginated()
|
||||
.Items(tracks)
|
||||
.PageSize(LQ_ITEMS_PER_PAGE)
|
||||
.CurrentPage(page)
|
||||
.AddFooter(false)
|
||||
.Page(PrintAction)
|
||||
.SendAsync();
|
||||
.Paginated()
|
||||
.Items(tracks)
|
||||
.PageSize(LQ_ITEMS_PER_PAGE)
|
||||
.CurrentPage(page)
|
||||
.AddFooter(false)
|
||||
.Page(PrintAction)
|
||||
.SendAsync();
|
||||
}
|
||||
|
||||
// search
|
||||
|
@ -353,15 +368,15 @@ public sealed partial class Music : EllieModule<IMusicService>
|
|||
|
||||
|
||||
var embeds = videos.Select((x, i) => CreateEmbed()
|
||||
.WithOkColor()
|
||||
.WithThumbnailUrl(x.Thumbnail)
|
||||
.WithDescription($"`{i + 1}.` {Format.Bold(x.Title)}\n\t{x.Url}"))
|
||||
.ToList();
|
||||
.WithOkColor()
|
||||
.WithThumbnailUrl(x.Thumbnail)
|
||||
.WithDescription($"`{i + 1}.` {Format.Bold(x.Title)}\n\t{x.Url}"))
|
||||
.ToList();
|
||||
|
||||
var msg = await Response()
|
||||
.Text(strs.queue_search_results)
|
||||
.Embeds(embeds)
|
||||
.SendAsync();
|
||||
.Text(strs.queue_search_results)
|
||||
.Embeds(embeds)
|
||||
.SendAsync();
|
||||
|
||||
try
|
||||
{
|
||||
|
@ -425,10 +440,10 @@ public sealed partial class Music : EllieModule<IMusicService>
|
|||
}
|
||||
|
||||
var embed = CreateEmbed()
|
||||
.WithAuthor(GetText(strs.removed_track) + " #" + index, MUSIC_ICON_URL)
|
||||
.WithDescription(track.PrettyName())
|
||||
.WithFooter(track.PrettyInfo())
|
||||
.WithErrorColor();
|
||||
.WithAuthor(GetText(strs.removed_track) + " #" + index, MUSIC_ICON_URL)
|
||||
.WithDescription(track.PrettyName())
|
||||
.WithFooter(track.PrettyInfo())
|
||||
.WithErrorColor();
|
||||
|
||||
await _service.SendToOutputAsync(ctx.Guild.Id, embed);
|
||||
}
|
||||
|
@ -593,11 +608,11 @@ public sealed partial class Music : EllieModule<IMusicService>
|
|||
}
|
||||
|
||||
var embed = CreateEmbed()
|
||||
.WithTitle(track.Title.TrimTo(65))
|
||||
.WithAuthor(GetText(strs.track_moved), MUSIC_ICON_URL)
|
||||
.AddField(GetText(strs.from_position), $"#{from + 1}", true)
|
||||
.AddField(GetText(strs.to_position), $"#{to + 1}", true)
|
||||
.WithOkColor();
|
||||
.WithTitle(track.Title.TrimTo(65))
|
||||
.WithAuthor(GetText(strs.track_moved), MUSIC_ICON_URL)
|
||||
.AddField(GetText(strs.from_position), $"#{from + 1}", true)
|
||||
.AddField(GetText(strs.to_position), $"#{to + 1}", true)
|
||||
.WithOkColor();
|
||||
|
||||
if (Uri.IsWellFormedUriString(track.Url, UriKind.Absolute))
|
||||
embed.WithUrl(track.Url);
|
||||
|
@ -652,12 +667,12 @@ public sealed partial class Music : EllieModule<IMusicService>
|
|||
return;
|
||||
|
||||
var embed = CreateEmbed()
|
||||
.WithOkColor()
|
||||
.WithAuthor(GetText(strs.now_playing), MUSIC_ICON_URL)
|
||||
.WithDescription(currentTrack.PrettyName())
|
||||
.WithThumbnailUrl(currentTrack.Thumbnail)
|
||||
.WithFooter(
|
||||
$"{mp.PrettyVolume()} | {mp.PrettyTotalTime()} | {currentTrack.Platform} | {currentTrack.Queuer}");
|
||||
.WithOkColor()
|
||||
.WithAuthor(GetText(strs.now_playing), MUSIC_ICON_URL)
|
||||
.WithDescription(currentTrack.PrettyName())
|
||||
.WithThumbnailUrl(currentTrack.Thumbnail)
|
||||
.WithFooter(
|
||||
$"{mp.PrettyVolume()} | {mp.PrettyTotalTime()} | {currentTrack.Platform} | {currentTrack.Queuer}");
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
[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
|
||||
xpratereset:
|
||||
- xpratereset
|
||||
- xpratereset
|
||||
lyrics:
|
||||
- lyrics
|
|
@ -4977,4 +4977,12 @@ xpratereset:
|
|||
params:
|
||||
- { }
|
||||
- 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_server_reset": "Server xp rate has been reset to global defaults.",
|
||||
"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
Reference in a new issue