From 5af0bff48603681dd7a781bc3df0bb977ff0eeb4 Mon Sep 17 00:00:00 2001 From: KMen1 Date: Sat, 9 Mar 2024 13:50:09 +0100 Subject: [PATCH 01/19] feat(LyricsJava): Add Lyrics.Java support --- .../GlobalUsings.cs | 1 + ...k4NET.Integrations.LyricsJava.Tests.csproj | 29 +++++ .../AlbumArt.cs | 6 + .../Converters/NumberTimeSpanJsonConverter.cs | 17 +++ .../Converters/StringUriJsonConverter.cs | 17 +++ .../Events/LyricsLoadedEventArgs.cs | 14 +++ .../Extensions/AudioServiceExtensions.cs | 13 ++ .../Extensions/HostExtensions.cs | 15 +++ .../Extensions/LavalinkApiClientExtensions.cs | 116 ++++++++++++++++++ .../ILyricsJavaIntegration.cs | 9 ++ ...avalink4NET.Integrations.LyricsJava.csproj | 18 +++ .../Lyrics.cs | 11 ++ .../LyricsJavaIntegration.cs | 69 +++++++++++ .../LyricsTrack.cs | 9 ++ .../LyricsType.cs | 8 ++ .../Models/AlbumArtModel.cs | 17 +++ .../Models/LyricsResponseModel.cs | 22 ++++ .../Models/LyricsResponseTrackModel.cs | 19 +++ .../Models/SearchResultModel.cs | 12 ++ .../Models/TimeRangeModel.cs | 15 +++ .../Models/TimedLyricsLineModel.cs | 12 ++ .../Players/ILavaLyricsPlayerListener.cs | 8 ++ .../TimeRange.cs | 5 + .../TimedLyricsLine.cs | 5 + src/Lavalink4NET.sln | 14 +++ 25 files changed, 481 insertions(+) create mode 100644 src/Lavalink4NET.Integrations.LyricsJava.Tests/GlobalUsings.cs create mode 100644 src/Lavalink4NET.Integrations.LyricsJava.Tests/Lavalink4NET.Integrations.LyricsJava.Tests.csproj create mode 100644 src/Lavalink4NET.Integrations.LyricsJava/AlbumArt.cs create mode 100644 src/Lavalink4NET.Integrations.LyricsJava/Converters/NumberTimeSpanJsonConverter.cs create mode 100644 src/Lavalink4NET.Integrations.LyricsJava/Converters/StringUriJsonConverter.cs create mode 100644 src/Lavalink4NET.Integrations.LyricsJava/Events/LyricsLoadedEventArgs.cs create mode 100644 src/Lavalink4NET.Integrations.LyricsJava/Extensions/AudioServiceExtensions.cs create mode 100644 src/Lavalink4NET.Integrations.LyricsJava/Extensions/HostExtensions.cs create mode 100644 src/Lavalink4NET.Integrations.LyricsJava/Extensions/LavalinkApiClientExtensions.cs create mode 100644 src/Lavalink4NET.Integrations.LyricsJava/ILyricsJavaIntegration.cs create mode 100644 src/Lavalink4NET.Integrations.LyricsJava/Lavalink4NET.Integrations.LyricsJava.csproj create mode 100644 src/Lavalink4NET.Integrations.LyricsJava/Lyrics.cs create mode 100644 src/Lavalink4NET.Integrations.LyricsJava/LyricsJavaIntegration.cs create mode 100644 src/Lavalink4NET.Integrations.LyricsJava/LyricsTrack.cs create mode 100644 src/Lavalink4NET.Integrations.LyricsJava/LyricsType.cs create mode 100644 src/Lavalink4NET.Integrations.LyricsJava/Models/AlbumArtModel.cs create mode 100644 src/Lavalink4NET.Integrations.LyricsJava/Models/LyricsResponseModel.cs create mode 100644 src/Lavalink4NET.Integrations.LyricsJava/Models/LyricsResponseTrackModel.cs create mode 100644 src/Lavalink4NET.Integrations.LyricsJava/Models/SearchResultModel.cs create mode 100644 src/Lavalink4NET.Integrations.LyricsJava/Models/TimeRangeModel.cs create mode 100644 src/Lavalink4NET.Integrations.LyricsJava/Models/TimedLyricsLineModel.cs create mode 100644 src/Lavalink4NET.Integrations.LyricsJava/Players/ILavaLyricsPlayerListener.cs create mode 100644 src/Lavalink4NET.Integrations.LyricsJava/TimeRange.cs create mode 100644 src/Lavalink4NET.Integrations.LyricsJava/TimedLyricsLine.cs diff --git a/src/Lavalink4NET.Integrations.LyricsJava.Tests/GlobalUsings.cs b/src/Lavalink4NET.Integrations.LyricsJava.Tests/GlobalUsings.cs new file mode 100644 index 00000000..5f282702 --- /dev/null +++ b/src/Lavalink4NET.Integrations.LyricsJava.Tests/GlobalUsings.cs @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/Lavalink4NET.Integrations.LyricsJava.Tests/Lavalink4NET.Integrations.LyricsJava.Tests.csproj b/src/Lavalink4NET.Integrations.LyricsJava.Tests/Lavalink4NET.Integrations.LyricsJava.Tests.csproj new file mode 100644 index 00000000..2fd7a6b5 --- /dev/null +++ b/src/Lavalink4NET.Integrations.LyricsJava.Tests/Lavalink4NET.Integrations.LyricsJava.Tests.csproj @@ -0,0 +1,29 @@ + + + + net7.0 + enable + enable + + false + true + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + diff --git a/src/Lavalink4NET.Integrations.LyricsJava/AlbumArt.cs b/src/Lavalink4NET.Integrations.LyricsJava/AlbumArt.cs new file mode 100644 index 00000000..7a227e39 --- /dev/null +++ b/src/Lavalink4NET.Integrations.LyricsJava/AlbumArt.cs @@ -0,0 +1,6 @@ +namespace Lavalink4NET.Integrations.LyricsJava; + +public sealed record class AlbumArt( + Uri Url, + int Width, + int Height); \ No newline at end of file diff --git a/src/Lavalink4NET.Integrations.LyricsJava/Converters/NumberTimeSpanJsonConverter.cs b/src/Lavalink4NET.Integrations.LyricsJava/Converters/NumberTimeSpanJsonConverter.cs new file mode 100644 index 00000000..3a9c627a --- /dev/null +++ b/src/Lavalink4NET.Integrations.LyricsJava/Converters/NumberTimeSpanJsonConverter.cs @@ -0,0 +1,17 @@ +namespace Lavalink4NET.Integrations.LyricsJava.Converters; + +using System.Text.Json; +using System.Text.Json.Serialization; + +public sealed class NumberTimeSpanJsonConverter : JsonConverter +{ + public override TimeSpan Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return TimeSpan.FromMilliseconds(reader.GetInt64()); + } + + public override void Write(Utf8JsonWriter writer, TimeSpan value, JsonSerializerOptions options) + { + writer.WriteNumberValue((long)value.TotalMilliseconds); + } +} diff --git a/src/Lavalink4NET.Integrations.LyricsJava/Converters/StringUriJsonConverter.cs b/src/Lavalink4NET.Integrations.LyricsJava/Converters/StringUriJsonConverter.cs new file mode 100644 index 00000000..2b8c640a --- /dev/null +++ b/src/Lavalink4NET.Integrations.LyricsJava/Converters/StringUriJsonConverter.cs @@ -0,0 +1,17 @@ +namespace Lavalink4NET.Integrations.LyricsJava.Converters; + +using System.Text.Json; +using System.Text.Json.Serialization; + +public sealed class StringUriJsonConverter : JsonConverter +{ + public override Uri Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return new Uri(reader.GetString() ?? string.Empty); + } + + public override void Write(Utf8JsonWriter writer, Uri value, JsonSerializerOptions options) + { + writer.WriteString("url", value.ToString()); + } +} diff --git a/src/Lavalink4NET.Integrations.LyricsJava/Events/LyricsLoadedEventArgs.cs b/src/Lavalink4NET.Integrations.LyricsJava/Events/LyricsLoadedEventArgs.cs new file mode 100644 index 00000000..242d83bb --- /dev/null +++ b/src/Lavalink4NET.Integrations.LyricsJava/Events/LyricsLoadedEventArgs.cs @@ -0,0 +1,14 @@ +namespace Lavalink4NET.Integrations.LyricsJava.Events; + +public class LyricsLoadedEventArgs : EventArgs +{ + public LyricsLoadedEventArgs(ulong guildId, Lyrics lyrics) + { + GuildId = guildId; + Lyrics = lyrics; + } + + public ulong GuildId { get; } + + public Lyrics Lyrics { get; } +} \ No newline at end of file diff --git a/src/Lavalink4NET.Integrations.LyricsJava/Extensions/AudioServiceExtensions.cs b/src/Lavalink4NET.Integrations.LyricsJava/Extensions/AudioServiceExtensions.cs new file mode 100644 index 00000000..637f8eca --- /dev/null +++ b/src/Lavalink4NET.Integrations.LyricsJava/Extensions/AudioServiceExtensions.cs @@ -0,0 +1,13 @@ +namespace Lavalink4NET.Integrations.LyricsJava.Extensions; + +public static class AudioServiceExtensions +{ + public static IAudioService UseLyricsJava(this IAudioService audioService) + { + ArgumentNullException.ThrowIfNull(audioService); + + audioService.Integrations.Set(new LyricsJavaIntegration(audioService)); + + return audioService; + } +} \ No newline at end of file diff --git a/src/Lavalink4NET.Integrations.LyricsJava/Extensions/HostExtensions.cs b/src/Lavalink4NET.Integrations.LyricsJava/Extensions/HostExtensions.cs new file mode 100644 index 00000000..456ffa40 --- /dev/null +++ b/src/Lavalink4NET.Integrations.LyricsJava/Extensions/HostExtensions.cs @@ -0,0 +1,15 @@ +namespace Lavalink4NET.Integrations.LyricsJava.Extensions; + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +public static class HostExtensions +{ + public static IHost UseLyricsJava(this IHost host) + { + ArgumentNullException.ThrowIfNull(host); + + host.Services.GetRequiredService().UseLyricsJava(); + return host; + } +} \ No newline at end of file diff --git a/src/Lavalink4NET.Integrations.LyricsJava/Extensions/LavalinkApiClientExtensions.cs b/src/Lavalink4NET.Integrations.LyricsJava/Extensions/LavalinkApiClientExtensions.cs new file mode 100644 index 00000000..a85b5f78 --- /dev/null +++ b/src/Lavalink4NET.Integrations.LyricsJava/Extensions/LavalinkApiClientExtensions.cs @@ -0,0 +1,116 @@ +namespace Lavalink4NET.Integrations.LyricsJava.Extensions; + +using System.Collections.Immutable; +using System.Net.Http.Json; +using System.Text.Json; +using Lavalink4NET.Integrations.LyricsJava.Models; +using Lavalink4NET.Rest; + +public static class LavalinkApiClientExtensions +{ + public static async ValueTask GetCurrentTrackLyricsAsync( + this ILavalinkApiClient apiClient, + string sessionId, + ulong guildId, + CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + ArgumentNullException.ThrowIfNull(sessionId); + ArgumentNullException.ThrowIfNull(guildId); + + var endpoint = apiClient.Endpoints.Build($"/v{apiClient.Endpoints.ApiVersion}/sessions/{sessionId}/players/{guildId}/lyrics"); + using var httpClient = apiClient.CreateHttpClient(); + + var response = await httpClient.GetAsync(endpoint, cancellationToken).ConfigureAwait(false); + + LyricsResponseModel? result; + try + { + result = await response.Content.ReadFromJsonAsync(cancellationToken: cancellationToken); + } + catch (JsonException) + { + result = null; + } + + return result; + } + + public static async ValueTask> SearchAsync( + this ILavalinkApiClient apiClient, + string query, + CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + ArgumentNullException.ThrowIfNull(query); + + var endpoint = apiClient.Endpoints.Build($"/v{apiClient.Endpoints.ApiVersion}/lyrics/search?query={query}"); + using var httpClient = apiClient.CreateHttpClient(); + + var response = await httpClient.GetAsync(endpoint, cancellationToken).ConfigureAwait(false); + + ImmutableArray result; + try + { + result = await response.Content.ReadFromJsonAsync>(cancellationToken: cancellationToken).ConfigureAwait(false); + } + catch (JsonException) + { + result = ImmutableArray.Empty; + } + + return result; + } + + public static async ValueTask GetYoutubeLyricsAsync( + this ILavalinkApiClient apiClient, + string videoId, + CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + ArgumentNullException.ThrowIfNull(videoId); + + var endpoint = apiClient.Endpoints.Build($"/v{apiClient.Endpoints.ApiVersion}/lyrics/{videoId}"); + using var httpClient = apiClient.CreateHttpClient(); + + var response = await httpClient.GetAsync(endpoint, cancellationToken).ConfigureAwait(false); + + LyricsResponseModel? result; + try + { + result = await response.Content.ReadFromJsonAsync(cancellationToken: cancellationToken); + } + catch (JsonException) + { + result = null; + } + + return result; + } + + public static async ValueTask GetGeniusLyricsAsync( + this ILavalinkApiClient apiClient, + string query, + CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + ArgumentNullException.ThrowIfNull(query); + + var endpoint = apiClient.Endpoints.Build($"/v{apiClient.Endpoints.ApiVersion}/lyrics/search?query={query}&source=genius"); + using var httpClient = apiClient.CreateHttpClient(); + + var response = await httpClient.GetAsync(endpoint, cancellationToken).ConfigureAwait(false); + + LyricsResponseModel? result; + try + { + result = await response.Content.ReadFromJsonAsync(cancellationToken: cancellationToken); + } + catch (JsonException) + { + result = null; + } + + return result; + } +} \ No newline at end of file diff --git a/src/Lavalink4NET.Integrations.LyricsJava/ILyricsJavaIntegration.cs b/src/Lavalink4NET.Integrations.LyricsJava/ILyricsJavaIntegration.cs new file mode 100644 index 00000000..c1b46ec9 --- /dev/null +++ b/src/Lavalink4NET.Integrations.LyricsJava/ILyricsJavaIntegration.cs @@ -0,0 +1,9 @@ +namespace Lavalink4NET.Integrations.LyricsJava; + +using Lavalink4NET.Events; +using Lavalink4NET.Integrations.LyricsJava.Events; + +public interface ILyricsJavaIntegration +{ + event AsyncEventHandler? LyricsLoaded; +} \ No newline at end of file diff --git a/src/Lavalink4NET.Integrations.LyricsJava/Lavalink4NET.Integrations.LyricsJava.csproj b/src/Lavalink4NET.Integrations.LyricsJava/Lavalink4NET.Integrations.LyricsJava.csproj new file mode 100644 index 00000000..5e1869ba --- /dev/null +++ b/src/Lavalink4NET.Integrations.LyricsJava/Lavalink4NET.Integrations.LyricsJava.csproj @@ -0,0 +1,18 @@ + + + + net6.0;net7.0 + enable + enable + + + High performance Lavalink wrapper for .NET | Expand your audio playback experience with adding support for the Lavasrc audio source manager which adds support for searching tracks on Spotify, Apple Music, Deezer, Yandex Music and Flowery TTS. + + + + + + + + + diff --git a/src/Lavalink4NET.Integrations.LyricsJava/Lyrics.cs b/src/Lavalink4NET.Integrations.LyricsJava/Lyrics.cs new file mode 100644 index 00000000..544e8564 --- /dev/null +++ b/src/Lavalink4NET.Integrations.LyricsJava/Lyrics.cs @@ -0,0 +1,11 @@ +namespace Lavalink4NET.Integrations.LyricsJava; + +using System.Collections.Immutable; + +public sealed record class Lyrics( + LyricsType Type, + string? Source, + string? Basic, + LyricsTrack? Track, + ImmutableArray? Timed +); \ No newline at end of file diff --git a/src/Lavalink4NET.Integrations.LyricsJava/LyricsJavaIntegration.cs b/src/Lavalink4NET.Integrations.LyricsJava/LyricsJavaIntegration.cs new file mode 100644 index 00000000..b527e6c2 --- /dev/null +++ b/src/Lavalink4NET.Integrations.LyricsJava/LyricsJavaIntegration.cs @@ -0,0 +1,69 @@ +namespace Lavalink4NET.Integrations.LyricsJava; + +using System.Collections.Immutable; +using Lavalink4NET.Events; +using Lavalink4NET.Integrations.LyricsJava.Events; +using Lavalink4NET.Integrations.LyricsJava.Extensions; +using Lavalink4NET.Integrations.LyricsJava.Models; +using Lavalink4NET.Integrations.LyricsJava.Players; +using Lavalink4NET.Protocol.Payloads; +using Lavalink4NET.Protocol.Payloads.Events; + +public class LyricsJavaIntegration : ILavalinkIntegration, ILyricsJavaIntegration +{ + private readonly IAudioService _audioService; + + public LyricsJavaIntegration(IAudioService audioService) + { + _audioService = audioService; + } + + public event AsyncEventHandler? LyricsLoaded; + + async ValueTask ILavalinkIntegration.ProcessPayloadAsync(IPayload payload, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + ArgumentNullException.ThrowIfNull(payload); + + static LyricsTrack CreateLyricsTrack(LyricsResponseTrackModel model) => new( + Title: model.Title, + Author: model.Author, + Album: model.Album, + AlbumArt: model.AlbumArt.Select(x => new AlbumArt(x.Url, x.Width, x.Height)).ToImmutableArray()); + + static Lyrics CreateLyrics(LyricsResponseModel? model) => new( + Type: model?.Type == "timed" ? model.Type == "text" ? LyricsType.Basic : LyricsType.Timed : LyricsType.NotFound, + Source: model?.Source, + Basic: model?.LyricsText, + Track: model?.Track is not null ? CreateLyricsTrack(model.Track) : null, + Timed: model?.TimedLines?.Select(x => new TimedLyricsLine(x.Line, new TimeRange(x.Range.Start, x.Range.End))).ToImmutableArray()); + + if (payload is TrackStartEventPayload trackStartEventPayload) + { + var player = await _audioService.Players.GetPlayerAsync(trackStartEventPayload.GuildId, cancellationToken) + .ConfigureAwait(false); + + if (player?.CurrentTrack is null) + { + return; + } + + var apiClient = await _audioService.ApiClientProvider.GetClientAsync(cancellationToken); + var lyricsResult = await apiClient.GetCurrentTrackLyricsAsync(player.SessionId, player.GuildId, cancellationToken).ConfigureAwait(false); + var lyrics = CreateLyrics(lyricsResult); + + var eventArgs = new LyricsLoadedEventArgs( + guildId: trackStartEventPayload.GuildId, + lyrics: lyrics); + + await LyricsLoaded + .InvokeAsync(this, eventArgs) + .ConfigureAwait(false); + + if (player is ILavaLyricsPlayerListener playerListener) + { + await playerListener.NotifyLyricsLoadedAsync(lyrics, cancellationToken).ConfigureAwait(false); + } + } + } +} \ No newline at end of file diff --git a/src/Lavalink4NET.Integrations.LyricsJava/LyricsTrack.cs b/src/Lavalink4NET.Integrations.LyricsJava/LyricsTrack.cs new file mode 100644 index 00000000..a0e43bff --- /dev/null +++ b/src/Lavalink4NET.Integrations.LyricsJava/LyricsTrack.cs @@ -0,0 +1,9 @@ +namespace Lavalink4NET.Integrations.LyricsJava; + +using System.Collections.Immutable; + +public sealed record class LyricsTrack( + string Title, + string Author, + string Album, + ImmutableArray AlbumArt); \ No newline at end of file diff --git a/src/Lavalink4NET.Integrations.LyricsJava/LyricsType.cs b/src/Lavalink4NET.Integrations.LyricsJava/LyricsType.cs new file mode 100644 index 00000000..182ceaba --- /dev/null +++ b/src/Lavalink4NET.Integrations.LyricsJava/LyricsType.cs @@ -0,0 +1,8 @@ +namespace Lavalink4NET.Integrations.LyricsJava; + +public enum LyricsType +{ + NotFound, + Basic, + Timed +} \ No newline at end of file diff --git a/src/Lavalink4NET.Integrations.LyricsJava/Models/AlbumArtModel.cs b/src/Lavalink4NET.Integrations.LyricsJava/Models/AlbumArtModel.cs new file mode 100644 index 00000000..c5c1793a --- /dev/null +++ b/src/Lavalink4NET.Integrations.LyricsJava/Models/AlbumArtModel.cs @@ -0,0 +1,17 @@ +namespace Lavalink4NET.Integrations.LyricsJava.Models; + +using System.Text.Json.Serialization; +using Lavalink4NET.Integrations.LyricsJava.Converters; + +public sealed record class AlbumArtModel +{ + [JsonPropertyName("url")] + [JsonConverter(typeof(StringUriJsonConverter))] + public Uri Url { get; set; } = null!; + + [JsonPropertyName("height")] + public int Height { get; set; } + + [JsonPropertyName("width")] + public int Width { get; set; } +} \ No newline at end of file diff --git a/src/Lavalink4NET.Integrations.LyricsJava/Models/LyricsResponseModel.cs b/src/Lavalink4NET.Integrations.LyricsJava/Models/LyricsResponseModel.cs new file mode 100644 index 00000000..cea3e155 --- /dev/null +++ b/src/Lavalink4NET.Integrations.LyricsJava/Models/LyricsResponseModel.cs @@ -0,0 +1,22 @@ +namespace Lavalink4NET.Integrations.LyricsJava.Models; + +using System.Collections.Immutable; +using System.Text.Json.Serialization; + +public sealed record class LyricsResponseModel +{ + [JsonPropertyName("type")] + public string Type { get; set; } = null!; + + [JsonPropertyName("track")] + public LyricsResponseTrackModel? Track { get; set; } + + [JsonPropertyName("source")] + public string Source { get; set; } = null!; + + [JsonPropertyName("text")] + public string? LyricsText { get; set; } + + [JsonPropertyName("lines")] + public ImmutableArray? TimedLines { get; set; } +} \ No newline at end of file diff --git a/src/Lavalink4NET.Integrations.LyricsJava/Models/LyricsResponseTrackModel.cs b/src/Lavalink4NET.Integrations.LyricsJava/Models/LyricsResponseTrackModel.cs new file mode 100644 index 00000000..79987742 --- /dev/null +++ b/src/Lavalink4NET.Integrations.LyricsJava/Models/LyricsResponseTrackModel.cs @@ -0,0 +1,19 @@ +namespace Lavalink4NET.Integrations.LyricsJava.Models; + +using System.Collections.Immutable; +using System.Text.Json.Serialization; + +public sealed record class LyricsResponseTrackModel +{ + [JsonPropertyName("title")] + public string Title { get; set; } = null!; + + [JsonPropertyName("author")] + public string Author { get; set; } = null!; + + [JsonPropertyName("album")] + public string Album { get; set; } = null!; + + [JsonPropertyName("albumArt")] + public ImmutableArray AlbumArt { get; set; } +} \ No newline at end of file diff --git a/src/Lavalink4NET.Integrations.LyricsJava/Models/SearchResultModel.cs b/src/Lavalink4NET.Integrations.LyricsJava/Models/SearchResultModel.cs new file mode 100644 index 00000000..84905ea2 --- /dev/null +++ b/src/Lavalink4NET.Integrations.LyricsJava/Models/SearchResultModel.cs @@ -0,0 +1,12 @@ +using System.Text.Json.Serialization; + +namespace Lavalink4NET.Integrations.LyricsJava.Models; + +public sealed record class SearchResultModel +{ + [JsonPropertyName("videoId")] + public string VideoId { get; set; } = null!; + + [JsonPropertyName("title")] + public string Title { get; set; } = null!; +} \ No newline at end of file diff --git a/src/Lavalink4NET.Integrations.LyricsJava/Models/TimeRangeModel.cs b/src/Lavalink4NET.Integrations.LyricsJava/Models/TimeRangeModel.cs new file mode 100644 index 00000000..f4cd23b7 --- /dev/null +++ b/src/Lavalink4NET.Integrations.LyricsJava/Models/TimeRangeModel.cs @@ -0,0 +1,15 @@ +using System.Text.Json.Serialization; +using Lavalink4NET.Integrations.LyricsJava.Converters; + +namespace Lavalink4NET.Integrations.LyricsJava.Models; + +public sealed record class TimeRangeModel +{ + [JsonPropertyName("start")] + [JsonConverter(typeof(NumberTimeSpanJsonConverter))] + public TimeSpan Start { get; set; } + + [JsonPropertyName("end")] + [JsonConverter(typeof(NumberTimeSpanJsonConverter))] + public TimeSpan End { get; set; } +} \ No newline at end of file diff --git a/src/Lavalink4NET.Integrations.LyricsJava/Models/TimedLyricsLineModel.cs b/src/Lavalink4NET.Integrations.LyricsJava/Models/TimedLyricsLineModel.cs new file mode 100644 index 00000000..6c9757cc --- /dev/null +++ b/src/Lavalink4NET.Integrations.LyricsJava/Models/TimedLyricsLineModel.cs @@ -0,0 +1,12 @@ +using System.Text.Json.Serialization; + +namespace Lavalink4NET.Integrations.LyricsJava.Models; + +public sealed record class TimedLyricsLineModel +{ + [JsonPropertyName("line")] + public string Line { get; set; } = null!; + + [JsonPropertyName("range")] + public TimeRangeModel Range { get; set; } = null!; +} \ No newline at end of file diff --git a/src/Lavalink4NET.Integrations.LyricsJava/Players/ILavaLyricsPlayerListener.cs b/src/Lavalink4NET.Integrations.LyricsJava/Players/ILavaLyricsPlayerListener.cs new file mode 100644 index 00000000..6546f901 --- /dev/null +++ b/src/Lavalink4NET.Integrations.LyricsJava/Players/ILavaLyricsPlayerListener.cs @@ -0,0 +1,8 @@ +namespace Lavalink4NET.Integrations.LyricsJava.Players; + +using Lavalink4NET.Players; + +public interface ILavaLyricsPlayerListener : ILavalinkPlayerListener +{ + ValueTask NotifyLyricsLoadedAsync(Lyrics lyrics, CancellationToken cancellationToken = default); +} \ No newline at end of file diff --git a/src/Lavalink4NET.Integrations.LyricsJava/TimeRange.cs b/src/Lavalink4NET.Integrations.LyricsJava/TimeRange.cs new file mode 100644 index 00000000..e5eac276 --- /dev/null +++ b/src/Lavalink4NET.Integrations.LyricsJava/TimeRange.cs @@ -0,0 +1,5 @@ +namespace Lavalink4NET.Integrations.LyricsJava; + +public sealed record class TimeRange( + TimeSpan Start, + TimeSpan End); \ No newline at end of file diff --git a/src/Lavalink4NET.Integrations.LyricsJava/TimedLyricsLine.cs b/src/Lavalink4NET.Integrations.LyricsJava/TimedLyricsLine.cs new file mode 100644 index 00000000..05c32930 --- /dev/null +++ b/src/Lavalink4NET.Integrations.LyricsJava/TimedLyricsLine.cs @@ -0,0 +1,5 @@ +namespace Lavalink4NET.Integrations.LyricsJava; + +public readonly record struct TimedLyricsLine( + string Line, + TimeRange Range); \ No newline at end of file diff --git a/src/Lavalink4NET.sln b/src/Lavalink4NET.sln index 2f898f6c..4aab41d1 100644 --- a/src/Lavalink4NET.sln +++ b/src/Lavalink4NET.sln @@ -92,6 +92,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lavalink4NET.NetCord", "Lav EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lavalink4NET.Samples.NetCord", "..\samples\Lavalink4NET.Samples.NetCord\Lavalink4NET.Samples.NetCord.csproj", "{02FE863F-D979-439A-9A51-C4EA69D58D29}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lavalink4NET.Integrations.LyricsJava", "Lavalink4NET.Integrations.LyricsJava\Lavalink4NET.Integrations.LyricsJava.csproj", "{9A30E985-6D67-41D4-A12F-F1ADCD2ED0FE}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lavalink4NET.Integrations.LyricsJava.Tests", "Lavalink4NET.Integrations.LyricsJava.Tests\Lavalink4NET.Integrations.LyricsJava.Tests.csproj", "{176B0345-DF57-42B4-A8FD-4E6436D9554C}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -246,6 +250,14 @@ Global {02FE863F-D979-439A-9A51-C4EA69D58D29}.Debug|Any CPU.Build.0 = Debug|Any CPU {02FE863F-D979-439A-9A51-C4EA69D58D29}.Release|Any CPU.ActiveCfg = Release|Any CPU {02FE863F-D979-439A-9A51-C4EA69D58D29}.Release|Any CPU.Build.0 = Release|Any CPU + {9A30E985-6D67-41D4-A12F-F1ADCD2ED0FE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9A30E985-6D67-41D4-A12F-F1ADCD2ED0FE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9A30E985-6D67-41D4-A12F-F1ADCD2ED0FE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9A30E985-6D67-41D4-A12F-F1ADCD2ED0FE}.Release|Any CPU.Build.0 = Release|Any CPU + {176B0345-DF57-42B4-A8FD-4E6436D9554C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {176B0345-DF57-42B4-A8FD-4E6436D9554C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {176B0345-DF57-42B4-A8FD-4E6436D9554C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {176B0345-DF57-42B4-A8FD-4E6436D9554C}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -272,6 +284,8 @@ Global {5779F765-5F0D-422C-984A-7E44EAE737C8} = {48ECDC71-B9E3-4086-8194-DA81B4667CA6} {8587F98B-CFE1-4559-9614-ED3B2B0C4F4E} = {5FAEC63E-9752-48C4-8BC9-B101E0DBDBD3} {02FE863F-D979-439A-9A51-C4EA69D58D29} = {B9402D29-5B12-4672-97B8-570A60C0F878} + {9A30E985-6D67-41D4-A12F-F1ADCD2ED0FE} = {48ECDC71-B9E3-4086-8194-DA81B4667CA6} + {176B0345-DF57-42B4-A8FD-4E6436D9554C} = {48ECDC71-B9E3-4086-8194-DA81B4667CA6} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {466A619D-5C4B-4A8F-9852-7A5322F160A2} From 39db1c14fd5d608257e35f862f63fd4c0db52da2 Mon Sep 17 00:00:00 2001 From: KMen1 Date: Sat, 9 Mar 2024 14:18:26 +0100 Subject: [PATCH 02/19] Update README.md --- README.md | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index ac739c87..3e9b2246 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,7 @@ [![Lavalink4NET Support Server Banner](https://discordapp.com/api/guilds/894533462428635146/embed.png?style=banner3)](https://discord.gg/cD4qTmnqRg) ### Features + - ⚖️ **Node Clustering / Load Balancing**
Distribute load across nodes for efficient and reliable audio playback (for large scale bots). - ✳️ **Extensible**
Customize and enhance features using plugins to match your bot's needs. @@ -25,11 +26,11 @@ - 🔌 **Fully Asynchronous Interface**
Effortlessly communicate with the Lavalink audio server without causing delays in your bot. All actions that can be offloaded are asynchronous and can be canceled at any time if needed. -- 📝 **Logging** *(optional)*
Enable insights for troubleshooting and debugging. +- 📝 **Logging** _(optional)_
Enable insights for troubleshooting and debugging. -- ⚡ **Request Caching** *(optional)*
Improve performance by reducing redundant requests. +- ⚡ **Request Caching** _(optional)_
Improve performance by reducing redundant requests. -- ⏱️ **Inactivity Tracking** *(optional)*
Monitor inactive players and disconnect them to save resources. +- ⏱️ **Inactivity Tracking** _(optional)_
Monitor inactive players and disconnect them to save resources. - 🖋️ **Supports Lavalink plugins**
Expand capabilities by integrating with Lavalink plugins. @@ -42,7 +43,7 @@ - 📊 **Statistics tracking support**
Lavalink4NET supports tracking and evaluation of node statistics. In clustering, node statistics can be used to evaluate the best node for efficient resource usage. - ➕ **Compatible with [DSharpPlus](https://github.com/DSharpPlus/DSharpPlus), [Discord.Net](https://github.com/discord-net/Discord.Net), [Remora](https://github.com/Remora/Remora.Discord), and [NetCord](https://github.com/KubaZ2/NetCord).**
Lavalink4NET has an adaptive client API, meaning it can support any discord client. Currently, DSharpPlus, Discord.Net, Remora and NetCord are supported out-of-the-box. - + ### Documentation > [!IMPORTANT] @@ -74,6 +75,8 @@ Lavalink4NET offers high flexibility and extensibility by providing an isolated - [**Lavalink4NET.Integrations.TextToSpeech**](https://www.nuget.org/packages/Lavalink4NET.Integrations.TextToSpeech/)   ![NuGet](https://img.shields.io/nuget/vpre/Lavalink4NET.Integrations.TextToSpeech.svg?style=flat-square)
Enable text-to-speech functionality in Lavalink4NET. Convert written text into spoken words, allowing your application to generate and play audio from text inputs. Requires the installation of the corresponding plugin on the Lavalink node. +- [**Lavalink4NET.Integrations.LyricsJava**](https://www.nuget.org/packages/Lavalink4NET.Integrations.TextToSpeech/)   ![NuGet](https://img.shields.io/nuget/vpre/Lavalink4NET.Integrations.TextToSpeech.svg?style=flat-square)
Fetch timed lyrics from youtube or non-timed lyrics from genius. Automatically fetches lyrics for the current track. Requires the installation of the corresponding plugin on the Lavalink node. + #### _Services_ - [**Lavalink4NET.Lyrics**](https://www.nuget.org/packages/Lavalink4NET.Lyrics/)   ![NuGet](https://img.shields.io/nuget/vpre/Lavalink4NET.Lyrics.svg?style=flat-square)
Fetch and display song lyrics from lyrics.ovh with this lyrics service integrated with Lavalink4NET. Enhance the music experience for your users. @@ -84,7 +87,7 @@ Lavalink4NET offers high flexibility and extensibility by providing an isolated #### _Core Components_ -- [**Lavalink4NET**](https://www.nuget.org/packages/Lavalink4NET/)   ![NuGet](https://img.shields.io/nuget/vpre/Lavalink4NET.svg?style=flat-square)
This core library is used to implement client wrappers. It is not intended for end users. Please use Lavalink4NET.Discord.Net, Lavalink4NET.DSharpPlus, Lavalink4NET.Remora.Discord or Lavalink4NET.NetCord instead. +- [**Lavalink4NET**](https://www.nuget.org/packages/Lavalink4NET/)   ![NuGet](https://img.shields.io/nuget/vpre/Lavalink4NET.svg?style=flat-square)
This core library is used to implement client wrappers. It is not intended for end users. Please use Lavalink4NET.Discord.Net, Lavalink4NET.DSharpPlus, Lavalink4NET.Remora.Discord or Lavalink4NET.NetCord instead. - [**Lavalink4NET.Abstractions**](https://www.nuget.org/packages/Lavalink4NET.Abstractions/)   ![NuGet](https://img.shields.io/nuget/vpre/Lavalink4NET.Abstractions.svg?style=flat-square)
General abstractions and common primitives for the Lavalink4NET client library. @@ -93,6 +96,7 @@ Lavalink4NET offers high flexibility and extensibility by providing an isolated - [**Lavalink4NET.Rest**](https://www.nuget.org/packages/Lavalink4NET.Rest/)   ![NuGet](https://img.shields.io/nuget/vpre/Lavalink4NET.Rest.svg?style=flat-square)
Easily interact with the Lavalink REST API using this REST API client primitives library. Build custom functionalities or integrate Lavalink4NET with other services. ### Prerequisites + - At least one lavalink node - At least .NET 6 @@ -124,7 +128,7 @@ var playerOptions = new LavalinkPlayerOptions }; await audioService.Players - .JoinAsync(, , playerOptions, stoppingToken) + .JoinAsync(, , playerOptions, stoppingToken) .ConfigureAwait(false); ``` From 1ad29adbdb24b84f41744bc8f9bcdbb6da50738e Mon Sep 17 00:00:00 2001 From: KMen1 Date: Sat, 9 Mar 2024 14:19:08 +0100 Subject: [PATCH 03/19] docs(LyricsJava): Add docs for Lyrics.java integration --- docs/docs/integrations/lyricsjava.md | 91 ++++++++++++++++++++++++++++ 1 file changed, 91 insertions(+) create mode 100644 docs/docs/integrations/lyricsjava.md diff --git a/docs/docs/integrations/lyricsjava.md b/docs/docs/integrations/lyricsjava.md new file mode 100644 index 00000000..2d18eb83 --- /dev/null +++ b/docs/docs/integrations/lyricsjava.md @@ -0,0 +1,91 @@ +# Lyrics.Java + +The Lyrics.java plugin for Lavalink allows you to fetch lyrics from youtube or genius. The plugin will automatically fetch lyrics for the current track. + +Lavalink4NET provides an integration for the Lyrics.Java plugin with the [`Lavalink4NET.Integrations.LyricsJava`](https://www.nuget.org/packages/Lavalink4NET.Integrations.LyricsJava) package. + +## Installation + +For using Lyrics.Java, you need to install the [`Lavalink4NET.Integrations.LyricsJava`](https://www.nuget.org/packages/Lavalink4NET.Integrations.LyricsJava) package. + +:::caution +You need to have the [LyricsJava](https://github.com/DuncteBot/java-timed-lyrics) plugin installed on your Lavalink server. +::: + +## Usage + +First, you need to integrate the LyricsJava plugin with Lavalink4NET. You can do this by calling `UseLyricsJava` on either the host or the audio service: + +```csharp +var app = builder.Build(); + +app.UseLyricsJava(); + +await app.RunAsync(); +``` + +That's it! The LyricsJava plugin is now integrated with Lavalink4NET. + +### Getting lyrics for the current track + +For getting the lyrics of the current track, you can use the `GetCurrentTrackLyricsAsync` method. This method will return the lyrics of the current track. The method requires the session id and the guild id both of which you can get from player properties. + +```csharp +var apiClient = await AudioService + .ApiClientProvider + .GetClientAsync() + .ConfigureAwait(false); + +var player = await audioService.Players.GetPlayerAsync(guildId).ConfigureAwait(false); + +var lyrics = await apiClient + .GetCurrentTrackLyricsAsync(player.SessionId, player.GuildId) + .ConfigureAwait(false); +``` + +### Getting lyrics from youtube + +For getting the lyrics of a youtube video, you can use the `GetYoutubeLyricsAsync` method. This method will return the lyrics of the youtube video. The method requires a youtube video id, which can be acquired by using the `SearchAsync` method if using a different provider (e.g. Spotify). + +```csharp +var apiClient = await AudioService + .ApiClientProvider + .GetClientAsync() + .ConfigureAwait(false); + +var results = await apiClient + .SearchAsync("Queen - Bohemian Rhapsody") + .ConfigureAwait(false); + +var videoId = results.First().VideoId; + +var lyrics = await apiClient + .GetYoutubeLyricsAsync(videoId) // Youtube Video Id (e.g. dQw4w9WgXcQ) + .ConfigureAwait(false); +``` + +### Getting lyrics from genius + +For getting the lyrics of a song from genius, you can use the `GetGeniusLyricsAsync` method. This method will return the lyrics of the song. The method requires the song name and the artist name. + +```csharp +var apiClient = await AudioService + .ApiClientProvider + .GetClientAsync() + .ConfigureAwait(false); + +var lyrics = await apiClient + .GetGeniusLyricsAsync("Queen - Bohemian Rhapsody") + .ConfigureAwait(false); +``` + +## Player listener + +Similar to the inactivity tracking service, the LyricsJava integration also implements a player listener for receiving event notifications. The player listener can be used to receive notifications for when the lyrics of the current track has been loaded. + +```csharp +public interface ILavaLyricsPlayerListener : ILavalinkPlayerListener +{ + ValueTask NotifyLyricsLoadedAsync(Lyrics lyrics, CancellationToken cancellationToken = default); +} +``` From 4c485d2a60ae41ed8ee1191bd207f531a3cfbdca Mon Sep 17 00:00:00 2001 From: KMen1 Date: Sat, 9 Mar 2024 14:23:34 +0100 Subject: [PATCH 04/19] Update README.md --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 3e9b2246..a3fe71a5 100644 --- a/README.md +++ b/README.md @@ -26,11 +26,11 @@ - 🔌 **Fully Asynchronous Interface**
Effortlessly communicate with the Lavalink audio server without causing delays in your bot. All actions that can be offloaded are asynchronous and can be canceled at any time if needed. -- 📝 **Logging** _(optional)_
Enable insights for troubleshooting and debugging. +- 📝 **Logging** *(optional)*
Enable insights for troubleshooting and debugging. -- ⚡ **Request Caching** _(optional)_
Improve performance by reducing redundant requests. +- ⚡ **Request Caching** *(optional)*
Improve performance by reducing redundant requests. -- ⏱️ **Inactivity Tracking** _(optional)_
Monitor inactive players and disconnect them to save resources. +- ⏱️ **Inactivity Tracking** *(optional)*
Monitor inactive players and disconnect them to save resources. - 🖋️ **Supports Lavalink plugins**
Expand capabilities by integrating with Lavalink plugins. @@ -75,7 +75,7 @@ Lavalink4NET offers high flexibility and extensibility by providing an isolated - [**Lavalink4NET.Integrations.TextToSpeech**](https://www.nuget.org/packages/Lavalink4NET.Integrations.TextToSpeech/)   ![NuGet](https://img.shields.io/nuget/vpre/Lavalink4NET.Integrations.TextToSpeech.svg?style=flat-square)
Enable text-to-speech functionality in Lavalink4NET. Convert written text into spoken words, allowing your application to generate and play audio from text inputs. Requires the installation of the corresponding plugin on the Lavalink node. -- [**Lavalink4NET.Integrations.LyricsJava**](https://www.nuget.org/packages/Lavalink4NET.Integrations.TextToSpeech/)   ![NuGet](https://img.shields.io/nuget/vpre/Lavalink4NET.Integrations.TextToSpeech.svg?style=flat-square)
Fetch timed lyrics from youtube or non-timed lyrics from genius. Automatically fetches lyrics for the current track. Requires the installation of the corresponding plugin on the Lavalink node. +- [**Lavalink4NET.Integrations.LyricsJava**](https://www.nuget.org/packages/Lavalink4NET.Integrations.LyricsJava/)   ![NuGet](https://img.shields.io/nuget/vpre/Lavalink4NET.Integrations.LyricsJava.svg?style=flat-square)
Fetch timed lyrics from youtube or non-timed lyrics from genius. Automatically fetches lyrics for the current track. Requires the installation of the corresponding plugin on the Lavalink node. #### _Services_ From fd7668e88f0c62ef4aa646a7fd68d7c35b7b9074 Mon Sep 17 00:00:00 2001 From: Angelo Breuer Date: Sat, 9 Mar 2024 14:31:45 +0100 Subject: [PATCH 05/19] style: Minor style adjustments to be consistent with the repository --- .../Extensions/LavalinkApiClientExtensions.cs | 56 ++++++++++++------- .../LyricsJavaIntegration.cs | 28 ++++++---- 2 files changed, 54 insertions(+), 30 deletions(-) diff --git a/src/Lavalink4NET.Integrations.LyricsJava/Extensions/LavalinkApiClientExtensions.cs b/src/Lavalink4NET.Integrations.LyricsJava/Extensions/LavalinkApiClientExtensions.cs index a85b5f78..d32806c0 100644 --- a/src/Lavalink4NET.Integrations.LyricsJava/Extensions/LavalinkApiClientExtensions.cs +++ b/src/Lavalink4NET.Integrations.LyricsJava/Extensions/LavalinkApiClientExtensions.cs @@ -21,21 +21,25 @@ public static class LavalinkApiClientExtensions var endpoint = apiClient.Endpoints.Build($"/v{apiClient.Endpoints.ApiVersion}/sessions/{sessionId}/players/{guildId}/lyrics"); using var httpClient = apiClient.CreateHttpClient(); - var response = await httpClient.GetAsync(endpoint, cancellationToken).ConfigureAwait(false); - + var response = await httpClient + .GetAsync(endpoint, cancellationToken) + .ConfigureAwait(false); + LyricsResponseModel? result; try { - result = await response.Content.ReadFromJsonAsync(cancellationToken: cancellationToken); + result = await response.Content + .ReadFromJsonAsync(cancellationToken: cancellationToken) + .ConfigureAwait(false); } catch (JsonException) { result = null; } - + return result; } - + public static async ValueTask> SearchAsync( this ILavalinkApiClient apiClient, string query, @@ -47,22 +51,26 @@ public static async ValueTask> SearchAsync( var endpoint = apiClient.Endpoints.Build($"/v{apiClient.Endpoints.ApiVersion}/lyrics/search?query={query}"); using var httpClient = apiClient.CreateHttpClient(); - var response = await httpClient.GetAsync(endpoint, cancellationToken).ConfigureAwait(false); - + var response = await httpClient + .GetAsync(endpoint, cancellationToken) + .ConfigureAwait(false); + ImmutableArray result; try { - result = await response.Content.ReadFromJsonAsync>(cancellationToken: cancellationToken).ConfigureAwait(false); + result = await response.Content + .ReadFromJsonAsync>(cancellationToken: cancellationToken) + .ConfigureAwait(false); } catch (JsonException) { result = ImmutableArray.Empty; } - + return result; } - - public static async ValueTask GetYoutubeLyricsAsync( + + public static async ValueTask GetYouTubeLyricsAsync( this ILavalinkApiClient apiClient, string videoId, CancellationToken cancellationToken = default) @@ -73,21 +81,25 @@ public static async ValueTask> SearchAsync( var endpoint = apiClient.Endpoints.Build($"/v{apiClient.Endpoints.ApiVersion}/lyrics/{videoId}"); using var httpClient = apiClient.CreateHttpClient(); - var response = await httpClient.GetAsync(endpoint, cancellationToken).ConfigureAwait(false); - + var response = await httpClient + .GetAsync(endpoint, cancellationToken) + .ConfigureAwait(false); + LyricsResponseModel? result; try { - result = await response.Content.ReadFromJsonAsync(cancellationToken: cancellationToken); + result = await response.Content + .ReadFromJsonAsync(cancellationToken: cancellationToken) + .ConfigureAwait(false); } catch (JsonException) { result = null; } - + return result; } - + public static async ValueTask GetGeniusLyricsAsync( this ILavalinkApiClient apiClient, string query, @@ -99,18 +111,22 @@ public static async ValueTask> SearchAsync( var endpoint = apiClient.Endpoints.Build($"/v{apiClient.Endpoints.ApiVersion}/lyrics/search?query={query}&source=genius"); using var httpClient = apiClient.CreateHttpClient(); - var response = await httpClient.GetAsync(endpoint, cancellationToken).ConfigureAwait(false); - + var response = await httpClient + .GetAsync(endpoint, cancellationToken) + .ConfigureAwait(false); + LyricsResponseModel? result; try { - result = await response.Content.ReadFromJsonAsync(cancellationToken: cancellationToken); + result = await response.Content + .ReadFromJsonAsync(cancellationToken: cancellationToken) + .ConfigureAwait(false); } catch (JsonException) { result = null; } - + return result; } } \ No newline at end of file diff --git a/src/Lavalink4NET.Integrations.LyricsJava/LyricsJavaIntegration.cs b/src/Lavalink4NET.Integrations.LyricsJava/LyricsJavaIntegration.cs index b527e6c2..4d97e31e 100644 --- a/src/Lavalink4NET.Integrations.LyricsJava/LyricsJavaIntegration.cs +++ b/src/Lavalink4NET.Integrations.LyricsJava/LyricsJavaIntegration.cs @@ -17,7 +17,7 @@ public LyricsJavaIntegration(IAudioService audioService) { _audioService = audioService; } - + public event AsyncEventHandler? LyricsLoaded; async ValueTask ILavalinkIntegration.ProcessPayloadAsync(IPayload payload, CancellationToken cancellationToken) @@ -30,14 +30,14 @@ async ValueTask ILavalinkIntegration.ProcessPayloadAsync(IPayload payload, Cance Author: model.Author, Album: model.Album, AlbumArt: model.AlbumArt.Select(x => new AlbumArt(x.Url, x.Width, x.Height)).ToImmutableArray()); - + static Lyrics CreateLyrics(LyricsResponseModel? model) => new( Type: model?.Type == "timed" ? model.Type == "text" ? LyricsType.Basic : LyricsType.Timed : LyricsType.NotFound, Source: model?.Source, Basic: model?.LyricsText, Track: model?.Track is not null ? CreateLyricsTrack(model.Track) : null, Timed: model?.TimedLines?.Select(x => new TimedLyricsLine(x.Line, new TimeRange(x.Range.Start, x.Range.End))).ToImmutableArray()); - + if (payload is TrackStartEventPayload trackStartEventPayload) { var player = await _audioService.Players.GetPlayerAsync(trackStartEventPayload.GuildId, cancellationToken) @@ -47,22 +47,30 @@ async ValueTask ILavalinkIntegration.ProcessPayloadAsync(IPayload payload, Cance { return; } - - var apiClient = await _audioService.ApiClientProvider.GetClientAsync(cancellationToken); - var lyricsResult = await apiClient.GetCurrentTrackLyricsAsync(player.SessionId, player.GuildId, cancellationToken).ConfigureAwait(false); + + var apiClient = await _audioService.ApiClientProvider + .GetClientAsync(cancellationToken) + .ConfigureAwait(false); + + var lyricsResult = await apiClient + .GetCurrentTrackLyricsAsync(player.SessionId, player.GuildId, cancellationToken) + .ConfigureAwait(false); + var lyrics = CreateLyrics(lyricsResult); var eventArgs = new LyricsLoadedEventArgs( guildId: trackStartEventPayload.GuildId, - lyrics: lyrics); - + lyrics: lyrics); + await LyricsLoaded .InvokeAsync(this, eventArgs) .ConfigureAwait(false); - + if (player is ILavaLyricsPlayerListener playerListener) { - await playerListener.NotifyLyricsLoadedAsync(lyrics, cancellationToken).ConfigureAwait(false); + await playerListener + .NotifyLyricsLoadedAsync(lyrics, cancellationToken) + .ConfigureAwait(false); } } } From 6558a7fe9840b59bd6f202b2feb43ec03b9f15c7 Mon Sep 17 00:00:00 2001 From: Angelo Breuer Date: Sat, 9 Mar 2024 14:34:39 +0100 Subject: [PATCH 06/19] feat: Use JSON source generator for LyricsJava --- .../Extensions/LavalinkApiClientExtensions.cs | 6 +++--- .../Models/ModelJsonSerializerContext.cs | 15 +++++++++++++++ 2 files changed, 18 insertions(+), 3 deletions(-) create mode 100644 src/Lavalink4NET.Integrations.LyricsJava/Models/ModelJsonSerializerContext.cs diff --git a/src/Lavalink4NET.Integrations.LyricsJava/Extensions/LavalinkApiClientExtensions.cs b/src/Lavalink4NET.Integrations.LyricsJava/Extensions/LavalinkApiClientExtensions.cs index d32806c0..c8e5cbbd 100644 --- a/src/Lavalink4NET.Integrations.LyricsJava/Extensions/LavalinkApiClientExtensions.cs +++ b/src/Lavalink4NET.Integrations.LyricsJava/Extensions/LavalinkApiClientExtensions.cs @@ -29,7 +29,7 @@ public static class LavalinkApiClientExtensions try { result = await response.Content - .ReadFromJsonAsync(cancellationToken: cancellationToken) + .ReadFromJsonAsync(ModelJsonSerializerContext.Default.LyricsResponseModel, cancellationToken: cancellationToken) .ConfigureAwait(false); } catch (JsonException) @@ -59,7 +59,7 @@ public static async ValueTask> SearchAsync( try { result = await response.Content - .ReadFromJsonAsync>(cancellationToken: cancellationToken) + .ReadFromJsonAsync(ModelJsonSerializerContext.Default.ImmutableArraySearchResultModel, cancellationToken: cancellationToken) .ConfigureAwait(false); } catch (JsonException) @@ -89,7 +89,7 @@ public static async ValueTask> SearchAsync( try { result = await response.Content - .ReadFromJsonAsync(cancellationToken: cancellationToken) + .ReadFromJsonAsync(ModelJsonSerializerContext.Default.LyricsResponseModel, cancellationToken: cancellationToken) .ConfigureAwait(false); } catch (JsonException) diff --git a/src/Lavalink4NET.Integrations.LyricsJava/Models/ModelJsonSerializerContext.cs b/src/Lavalink4NET.Integrations.LyricsJava/Models/ModelJsonSerializerContext.cs new file mode 100644 index 00000000..2d5c056f --- /dev/null +++ b/src/Lavalink4NET.Integrations.LyricsJava/Models/ModelJsonSerializerContext.cs @@ -0,0 +1,15 @@ +namespace Lavalink4NET.Integrations.LyricsJava.Models; + +using System.Collections.Immutable; +using System.Text.Json.Serialization; + +[JsonSerializable(typeof(AlbumArtModel))] +[JsonSerializable(typeof(LyricsResponseModel))] +[JsonSerializable(typeof(ImmutableArray))] +[JsonSerializable(typeof(LyricsResponseTrackModel))] +[JsonSerializable(typeof(SearchResultModel))] +[JsonSerializable(typeof(TimedLyricsLineModel))] +[JsonSerializable(typeof(TimeRangeModel))] +internal sealed partial class ModelJsonSerializerContext : JsonSerializerContext +{ +} From 20c372cb61dd98e5f7bb6e46c64f2f11f50700bf Mon Sep 17 00:00:00 2001 From: Angelo Breuer Date: Sat, 9 Mar 2024 14:35:48 +0100 Subject: [PATCH 07/19] style: Minor code style adjustment --- src/Lavalink4NET.Integrations.LyricsJava/Lyrics.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Lavalink4NET.Integrations.LyricsJava/Lyrics.cs b/src/Lavalink4NET.Integrations.LyricsJava/Lyrics.cs index 544e8564..60fc2672 100644 --- a/src/Lavalink4NET.Integrations.LyricsJava/Lyrics.cs +++ b/src/Lavalink4NET.Integrations.LyricsJava/Lyrics.cs @@ -7,5 +7,4 @@ public sealed record class Lyrics( string? Source, string? Basic, LyricsTrack? Track, - ImmutableArray? Timed -); \ No newline at end of file + ImmutableArray? Timed); \ No newline at end of file From ae7a1531be5f8049e8d009fde041b3470fc99a50 Mon Sep 17 00:00:00 2001 From: Angelo Breuer Date: Sat, 9 Mar 2024 14:39:09 +0100 Subject: [PATCH 08/19] style: Adjust code style in docs --- docs/docs/integrations/lyricsjava.md | 29 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/docs/docs/integrations/lyricsjava.md b/docs/docs/integrations/lyricsjava.md index 2d18eb83..9e1467d4 100644 --- a/docs/docs/integrations/lyricsjava.md +++ b/docs/docs/integrations/lyricsjava.md @@ -1,6 +1,6 @@ # Lyrics.Java -The Lyrics.java plugin for Lavalink allows you to fetch lyrics from youtube or genius. The plugin will automatically fetch lyrics for the current track. +The Lyrics.Java plugin for Lavalink allows you to fetch lyrics from YouTube or genius. The plugin will automatically fetch lyrics for the current track. Lavalink4NET provides an integration for the Lyrics.Java plugin with the [`Lavalink4NET.Integrations.LyricsJava`](https://www.nuget.org/packages/Lavalink4NET.Integrations.LyricsJava) package. @@ -21,7 +21,7 @@ var app = builder.Build(); app.UseLyricsJava(); -await app.RunAsync(); +app.Run(); ``` That's it! The LyricsJava plugin is now integrated with Lavalink4NET. @@ -31,12 +31,13 @@ That's it! The LyricsJava plugin is now integrated with Lavalink4NET. For getting the lyrics of the current track, you can use the `GetCurrentTrackLyricsAsync` method. This method will return the lyrics of the current track. The method requires the session id and the guild id both of which you can get from player properties. ```csharp -var apiClient = await AudioService - .ApiClientProvider - .GetClientAsync() - .ConfigureAwait(false); +var apiClient = await AudioService.ApiClientProvider + .GetClientAsync() + .ConfigureAwait(false); -var player = await audioService.Players.GetPlayerAsync(guildId).ConfigureAwait(false); +var player = await audioService.Players + .GetPlayerAsync(guildId) + .ConfigureAwait(false); var lyrics = await apiClient .GetCurrentTrackLyricsAsync(player.SessionId, player.GuildId) @@ -48,10 +49,9 @@ var lyrics = await apiClient For getting the lyrics of a youtube video, you can use the `GetYoutubeLyricsAsync` method. This method will return the lyrics of the youtube video. The method requires a youtube video id, which can be acquired by using the `SearchAsync` method if using a different provider (e.g. Spotify). ```csharp -var apiClient = await AudioService - .ApiClientProvider - .GetClientAsync() - .ConfigureAwait(false); +var apiClient = await AudioService.ApiClientProvider + .GetClientAsync() + .ConfigureAwait(false); var results = await apiClient .SearchAsync("Queen - Bohemian Rhapsody") @@ -69,10 +69,9 @@ var lyrics = await apiClient For getting the lyrics of a song from genius, you can use the `GetGeniusLyricsAsync` method. This method will return the lyrics of the song. The method requires the song name and the artist name. ```csharp -var apiClient = await AudioService - .ApiClientProvider - .GetClientAsync() - .ConfigureAwait(false); +var apiClient = await AudioService.ApiClientProvider + .GetClientAsync() + .ConfigureAwait(false); var lyrics = await apiClient .GetGeniusLyricsAsync("Queen - Bohemian Rhapsody") From 9754ccfbfaca9cbfcdd1cec36dc0e9eb5a100097 Mon Sep 17 00:00:00 2001 From: Angelo Breuer Date: Sat, 9 Mar 2024 14:45:22 +0100 Subject: [PATCH 09/19] perf: Use HttpCompletionOption.ResponseHeadersRead for lyrics GET --- .../Extensions/LavalinkApiClientExtensions.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Lavalink4NET.Integrations.LyricsJava/Extensions/LavalinkApiClientExtensions.cs b/src/Lavalink4NET.Integrations.LyricsJava/Extensions/LavalinkApiClientExtensions.cs index c8e5cbbd..73468c8b 100644 --- a/src/Lavalink4NET.Integrations.LyricsJava/Extensions/LavalinkApiClientExtensions.cs +++ b/src/Lavalink4NET.Integrations.LyricsJava/Extensions/LavalinkApiClientExtensions.cs @@ -22,7 +22,7 @@ public static class LavalinkApiClientExtensions using var httpClient = apiClient.CreateHttpClient(); var response = await httpClient - .GetAsync(endpoint, cancellationToken) + .GetAsync(endpoint, completionOption: HttpCompletionOption.ResponseHeadersRead, cancellationToken) .ConfigureAwait(false); LyricsResponseModel? result; @@ -52,7 +52,7 @@ public static async ValueTask> SearchAsync( using var httpClient = apiClient.CreateHttpClient(); var response = await httpClient - .GetAsync(endpoint, cancellationToken) + .GetAsync(endpoint, completionOption: HttpCompletionOption.ResponseHeadersRead, cancellationToken) .ConfigureAwait(false); ImmutableArray result; @@ -82,7 +82,7 @@ public static async ValueTask> SearchAsync( using var httpClient = apiClient.CreateHttpClient(); var response = await httpClient - .GetAsync(endpoint, cancellationToken) + .GetAsync(endpoint, completionOption: HttpCompletionOption.ResponseHeadersRead, cancellationToken) .ConfigureAwait(false); LyricsResponseModel? result; @@ -112,7 +112,7 @@ public static async ValueTask> SearchAsync( using var httpClient = apiClient.CreateHttpClient(); var response = await httpClient - .GetAsync(endpoint, cancellationToken) + .GetAsync(endpoint, completionOption: HttpCompletionOption.ResponseHeadersRead, cancellationToken) .ConfigureAwait(false); LyricsResponseModel? result; From 7062bd97a84c0e59f977d0aab4b52b00ce1554f7 Mon Sep 17 00:00:00 2001 From: Angelo Breuer Date: Sat, 9 Mar 2024 14:48:00 +0100 Subject: [PATCH 10/19] refactor: Return proper entity instead of API model for lyrics --- .../Extensions/LavalinkApiClientExtensions.cs | 18 +++++---- .../LyricsJavaIntegration.cs | 39 ++++++++++--------- .../LyricsSearchResult.cs | 3 ++ 3 files changed, 33 insertions(+), 27 deletions(-) create mode 100644 src/Lavalink4NET.Integrations.LyricsJava/LyricsSearchResult.cs diff --git a/src/Lavalink4NET.Integrations.LyricsJava/Extensions/LavalinkApiClientExtensions.cs b/src/Lavalink4NET.Integrations.LyricsJava/Extensions/LavalinkApiClientExtensions.cs index 73468c8b..dbd7bef1 100644 --- a/src/Lavalink4NET.Integrations.LyricsJava/Extensions/LavalinkApiClientExtensions.cs +++ b/src/Lavalink4NET.Integrations.LyricsJava/Extensions/LavalinkApiClientExtensions.cs @@ -8,7 +8,7 @@ public static class LavalinkApiClientExtensions { - public static async ValueTask GetCurrentTrackLyricsAsync( + public static async ValueTask GetCurrentTrackLyricsAsync( this ILavalinkApiClient apiClient, string sessionId, ulong guildId, @@ -37,10 +37,10 @@ public static class LavalinkApiClientExtensions result = null; } - return result; + return LyricsJavaIntegration.CreateLyrics(result); } - public static async ValueTask> SearchAsync( + public static async ValueTask> SearchAsync( this ILavalinkApiClient apiClient, string query, CancellationToken cancellationToken = default) @@ -67,10 +67,12 @@ public static async ValueTask> SearchAsync( result = ImmutableArray.Empty; } - return result; + return result + .Select(x => new LyricsSearchResult(x.VideoId, x.Title)) + .ToImmutableArray(); } - public static async ValueTask GetYouTubeLyricsAsync( + public static async ValueTask GetYouTubeLyricsAsync( this ILavalinkApiClient apiClient, string videoId, CancellationToken cancellationToken = default) @@ -97,10 +99,10 @@ public static async ValueTask> SearchAsync( result = null; } - return result; + return LyricsJavaIntegration.CreateLyrics(result); } - public static async ValueTask GetGeniusLyricsAsync( + public static async ValueTask GetGeniusLyricsAsync( this ILavalinkApiClient apiClient, string query, CancellationToken cancellationToken = default) @@ -127,6 +129,6 @@ public static async ValueTask> SearchAsync( result = null; } - return result; + return LyricsJavaIntegration.CreateLyrics(result); } } \ No newline at end of file diff --git a/src/Lavalink4NET.Integrations.LyricsJava/LyricsJavaIntegration.cs b/src/Lavalink4NET.Integrations.LyricsJava/LyricsJavaIntegration.cs index 4d97e31e..a5137855 100644 --- a/src/Lavalink4NET.Integrations.LyricsJava/LyricsJavaIntegration.cs +++ b/src/Lavalink4NET.Integrations.LyricsJava/LyricsJavaIntegration.cs @@ -15,6 +15,8 @@ public class LyricsJavaIntegration : ILavalinkIntegration, ILyricsJavaIntegratio public LyricsJavaIntegration(IAudioService audioService) { + ArgumentNullException.ThrowIfNull(audioService); + _audioService = audioService; } @@ -25,25 +27,13 @@ async ValueTask ILavalinkIntegration.ProcessPayloadAsync(IPayload payload, Cance cancellationToken.ThrowIfCancellationRequested(); ArgumentNullException.ThrowIfNull(payload); - static LyricsTrack CreateLyricsTrack(LyricsResponseTrackModel model) => new( - Title: model.Title, - Author: model.Author, - Album: model.Album, - AlbumArt: model.AlbumArt.Select(x => new AlbumArt(x.Url, x.Width, x.Height)).ToImmutableArray()); - - static Lyrics CreateLyrics(LyricsResponseModel? model) => new( - Type: model?.Type == "timed" ? model.Type == "text" ? LyricsType.Basic : LyricsType.Timed : LyricsType.NotFound, - Source: model?.Source, - Basic: model?.LyricsText, - Track: model?.Track is not null ? CreateLyricsTrack(model.Track) : null, - Timed: model?.TimedLines?.Select(x => new TimedLyricsLine(x.Line, new TimeRange(x.Range.Start, x.Range.End))).ToImmutableArray()); - if (payload is TrackStartEventPayload trackStartEventPayload) { - var player = await _audioService.Players.GetPlayerAsync(trackStartEventPayload.GuildId, cancellationToken) + var player = await _audioService.Players + .GetPlayerAsync(trackStartEventPayload.GuildId, cancellationToken) .ConfigureAwait(false); - if (player?.CurrentTrack is null) + if (player is null) { return; } @@ -56,11 +46,9 @@ async ValueTask ILavalinkIntegration.ProcessPayloadAsync(IPayload payload, Cance .GetCurrentTrackLyricsAsync(player.SessionId, player.GuildId, cancellationToken) .ConfigureAwait(false); - var lyrics = CreateLyrics(lyricsResult); - var eventArgs = new LyricsLoadedEventArgs( guildId: trackStartEventPayload.GuildId, - lyrics: lyrics); + lyrics: lyricsResult); await LyricsLoaded .InvokeAsync(this, eventArgs) @@ -69,9 +57,22 @@ await LyricsLoaded if (player is ILavaLyricsPlayerListener playerListener) { await playerListener - .NotifyLyricsLoadedAsync(lyrics, cancellationToken) + .NotifyLyricsLoadedAsync(lyricsResult, cancellationToken) .ConfigureAwait(false); } } } + + internal static LyricsTrack CreateLyricsTrack(LyricsResponseTrackModel model) => new( + Title: model.Title, + Author: model.Author, + Album: model.Album, + AlbumArt: model.AlbumArt.Select(x => new AlbumArt(x.Url, x.Width, x.Height)).ToImmutableArray()); + + internal static Lyrics CreateLyrics(LyricsResponseModel? model) => new( + Type: model?.Type == "timed" ? model.Type == "text" ? LyricsType.Basic : LyricsType.Timed : LyricsType.NotFound, + Source: model?.Source, + Basic: model?.LyricsText, + Track: model?.Track is not null ? CreateLyricsTrack(model.Track) : null, + Timed: model?.TimedLines?.Select(x => new TimedLyricsLine(x.Line, new TimeRange(x.Range.Start, x.Range.End))).ToImmutableArray()); } \ No newline at end of file diff --git a/src/Lavalink4NET.Integrations.LyricsJava/LyricsSearchResult.cs b/src/Lavalink4NET.Integrations.LyricsJava/LyricsSearchResult.cs new file mode 100644 index 00000000..8650d5a9 --- /dev/null +++ b/src/Lavalink4NET.Integrations.LyricsJava/LyricsSearchResult.cs @@ -0,0 +1,3 @@ +namespace Lavalink4NET.Integrations.LyricsJava; + +public readonly record struct LyricsSearchResult(string VideoId, string Title); \ No newline at end of file From 69149a80ba7e105706cc22facf0eb76f2deb81bc Mon Sep 17 00:00:00 2001 From: Angelo Breuer Date: Sat, 9 Mar 2024 14:53:39 +0100 Subject: [PATCH 11/19] refactor!: Make auto-resolution for lyrics opt-in --- .../Extensions/AudioServiceExtensions.cs | 34 +++++++++++++++++-- .../Extensions/HostExtensions.cs | 30 ++++++++++++++-- .../LyricsJavaIntegration.cs | 8 +++-- .../LyricsJavaIntegrationOptions.cs | 6 ++++ 4 files changed, 70 insertions(+), 8 deletions(-) create mode 100644 src/Lavalink4NET.Integrations.LyricsJava/LyricsJavaIntegrationOptions.cs diff --git a/src/Lavalink4NET.Integrations.LyricsJava/Extensions/AudioServiceExtensions.cs b/src/Lavalink4NET.Integrations.LyricsJava/Extensions/AudioServiceExtensions.cs index 637f8eca..af24c238 100644 --- a/src/Lavalink4NET.Integrations.LyricsJava/Extensions/AudioServiceExtensions.cs +++ b/src/Lavalink4NET.Integrations.LyricsJava/Extensions/AudioServiceExtensions.cs @@ -1,13 +1,41 @@ namespace Lavalink4NET.Integrations.LyricsJava.Extensions; +using Microsoft.Extensions.Options; + public static class AudioServiceExtensions { - public static IAudioService UseLyricsJava(this IAudioService audioService) + public static IAudioService UseLyricsJava(this IAudioService audioService, IOptions options) { ArgumentNullException.ThrowIfNull(audioService); - - audioService.Integrations.Set(new LyricsJavaIntegration(audioService)); + ArgumentNullException.ThrowIfNull(options); + + audioService.Integrations.Set(new LyricsJavaIntegration(audioService, options)); return audioService; } + + public static IAudioService UseLyricsJava(this IAudioService audioService, Action? configure) + { + ArgumentNullException.ThrowIfNull(audioService); + + var options = new LyricsJavaIntegrationOptions(); + configure?.Invoke(options); + + return audioService.UseLyricsJava(Options.Create(options)); + } + + public static IAudioService UseLyricsJava(this IAudioService audioService, LyricsJavaIntegrationOptions options) + { + ArgumentNullException.ThrowIfNull(audioService); + ArgumentNullException.ThrowIfNull(options); + + return audioService.UseLyricsJava(Options.Create(options)); + } + + public static IAudioService UseLyricsJava(this IAudioService audioService) + { + ArgumentNullException.ThrowIfNull(audioService); + + return audioService.UseLyricsJava(Options.Create(new LyricsJavaIntegrationOptions())); + } } \ No newline at end of file diff --git a/src/Lavalink4NET.Integrations.LyricsJava/Extensions/HostExtensions.cs b/src/Lavalink4NET.Integrations.LyricsJava/Extensions/HostExtensions.cs index 456ffa40..ed145951 100644 --- a/src/Lavalink4NET.Integrations.LyricsJava/Extensions/HostExtensions.cs +++ b/src/Lavalink4NET.Integrations.LyricsJava/Extensions/HostExtensions.cs @@ -2,14 +2,38 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Options; public static class HostExtensions { - public static IHost UseLyricsJava(this IHost host) + public static IAudioService UseLyricsJava(this IHost host, IOptions options) { ArgumentNullException.ThrowIfNull(host); + ArgumentNullException.ThrowIfNull(options); - host.Services.GetRequiredService().UseLyricsJava(); - return host; + return host.Services.GetRequiredService().UseLyricsJava(options); + } + + public static IAudioService UseLyricsJava(this IHost host, Action? configure) + { + ArgumentNullException.ThrowIfNull(host); + ArgumentNullException.ThrowIfNull(configure); + + return host.Services.GetRequiredService().UseLyricsJava(configure); + } + + public static IAudioService UseLyricsJava(this IHost host, LyricsJavaIntegrationOptions options) + { + ArgumentNullException.ThrowIfNull(host); + ArgumentNullException.ThrowIfNull(options); + + return host.Services.GetRequiredService().UseLyricsJava(options); + } + + public static IAudioService UseLyricsJava(this IHost host) + { + ArgumentNullException.ThrowIfNull(host); + + return host.Services.GetRequiredService().UseLyricsJava(); } } \ No newline at end of file diff --git a/src/Lavalink4NET.Integrations.LyricsJava/LyricsJavaIntegration.cs b/src/Lavalink4NET.Integrations.LyricsJava/LyricsJavaIntegration.cs index a5137855..7a7d3d10 100644 --- a/src/Lavalink4NET.Integrations.LyricsJava/LyricsJavaIntegration.cs +++ b/src/Lavalink4NET.Integrations.LyricsJava/LyricsJavaIntegration.cs @@ -8,16 +8,20 @@ using Lavalink4NET.Integrations.LyricsJava.Players; using Lavalink4NET.Protocol.Payloads; using Lavalink4NET.Protocol.Payloads.Events; +using Microsoft.Extensions.Options; public class LyricsJavaIntegration : ILavalinkIntegration, ILyricsJavaIntegration { private readonly IAudioService _audioService; + private readonly LyricsJavaIntegrationOptions _options; - public LyricsJavaIntegration(IAudioService audioService) + public LyricsJavaIntegration(IAudioService audioService, IOptions options) { ArgumentNullException.ThrowIfNull(audioService); + ArgumentNullException.ThrowIfNull(options); _audioService = audioService; + _options = options.Value; } public event AsyncEventHandler? LyricsLoaded; @@ -27,7 +31,7 @@ async ValueTask ILavalinkIntegration.ProcessPayloadAsync(IPayload payload, Cance cancellationToken.ThrowIfCancellationRequested(); ArgumentNullException.ThrowIfNull(payload); - if (payload is TrackStartEventPayload trackStartEventPayload) + if (_options.AutoResolve && payload is TrackStartEventPayload trackStartEventPayload) { var player = await _audioService.Players .GetPlayerAsync(trackStartEventPayload.GuildId, cancellationToken) diff --git a/src/Lavalink4NET.Integrations.LyricsJava/LyricsJavaIntegrationOptions.cs b/src/Lavalink4NET.Integrations.LyricsJava/LyricsJavaIntegrationOptions.cs new file mode 100644 index 00000000..5f5bf5e0 --- /dev/null +++ b/src/Lavalink4NET.Integrations.LyricsJava/LyricsJavaIntegrationOptions.cs @@ -0,0 +1,6 @@ +namespace Lavalink4NET.Integrations.LyricsJava; + +public sealed record class LyricsJavaIntegrationOptions +{ + public bool AutoResolve { get; set; } = false; +} From 603a37964ac2442117886094cd9d4be9e7b40055 Mon Sep 17 00:00:00 2001 From: Angelo Breuer Date: Sat, 9 Mar 2024 15:00:56 +0100 Subject: [PATCH 12/19] feat: Add extensions for easy lyrics ITrackManager resolution --- .../Extensions/LavalinkApiClientExtensions.cs | 2 +- .../Extensions/TrackManagerExtensions.cs | 85 +++++++++++++++++++ 2 files changed, 86 insertions(+), 1 deletion(-) create mode 100644 src/Lavalink4NET.Integrations.LyricsJava/Extensions/TrackManagerExtensions.cs diff --git a/src/Lavalink4NET.Integrations.LyricsJava/Extensions/LavalinkApiClientExtensions.cs b/src/Lavalink4NET.Integrations.LyricsJava/Extensions/LavalinkApiClientExtensions.cs index dbd7bef1..4fcb7218 100644 --- a/src/Lavalink4NET.Integrations.LyricsJava/Extensions/LavalinkApiClientExtensions.cs +++ b/src/Lavalink4NET.Integrations.LyricsJava/Extensions/LavalinkApiClientExtensions.cs @@ -40,7 +40,7 @@ public static async ValueTask GetCurrentTrackLyricsAsync( return LyricsJavaIntegration.CreateLyrics(result); } - public static async ValueTask> SearchAsync( + public static async ValueTask> SearchLyricsAsync( this ILavalinkApiClient apiClient, string query, CancellationToken cancellationToken = default) diff --git a/src/Lavalink4NET.Integrations.LyricsJava/Extensions/TrackManagerExtensions.cs b/src/Lavalink4NET.Integrations.LyricsJava/Extensions/TrackManagerExtensions.cs new file mode 100644 index 00000000..91576b56 --- /dev/null +++ b/src/Lavalink4NET.Integrations.LyricsJava/Extensions/TrackManagerExtensions.cs @@ -0,0 +1,85 @@ +namespace Lavalink4NET.Integrations.LyricsJava.Extensions; + +using System.Collections.Immutable; +using System.Threading.Tasks; +using Lavalink4NET.Players; +using Lavalink4NET.Tracks; + +public static class TrackManagerExtensions +{ + public static async ValueTask GetCurrentTrackLyricsAsync( + this ITrackManager trackManager, + ILavalinkPlayer player, + CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + + ArgumentNullException.ThrowIfNull(trackManager); + ArgumentNullException.ThrowIfNull(player); + + var apiClient = await trackManager.ApiClientProvider + .GetClientAsync(cancellationToken) + .ConfigureAwait(false); + + return await apiClient + .GetCurrentTrackLyricsAsync(player.SessionId, player.GuildId, cancellationToken) + .ConfigureAwait(false); + } + + public static async ValueTask> SearchLyricsAsync( + this ITrackManager trackManager, + string query, + CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + + ArgumentNullException.ThrowIfNull(trackManager); + ArgumentNullException.ThrowIfNull(query); + + var apiClient = await trackManager.ApiClientProvider + .GetClientAsync(cancellationToken) + .ConfigureAwait(false); + + return await apiClient + .SearchLyricsAsync(query, cancellationToken) + .ConfigureAwait(false); + } + + public static async ValueTask GetYouTubeLyricsAsync( + this ITrackManager trackManager, + string query, + CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + + ArgumentNullException.ThrowIfNull(trackManager); + ArgumentNullException.ThrowIfNull(query); + + var apiClient = await trackManager.ApiClientProvider + .GetClientAsync(cancellationToken) + .ConfigureAwait(false); + + return await apiClient + .GetYouTubeLyricsAsync(query, cancellationToken) + .ConfigureAwait(false); + } + + public static async ValueTask GetGeniusLyricsAsync( + this ITrackManager trackManager, + string query, + CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + + ArgumentNullException.ThrowIfNull(trackManager); + ArgumentNullException.ThrowIfNull(query); + + var apiClient = await trackManager.ApiClientProvider + .GetClientAsync(cancellationToken) + .ConfigureAwait(false); + + return await apiClient + .GetGeniusLyricsAsync(query, cancellationToken) + .ConfigureAwait(false); + } +} From 61df619990ea20c855d4ee357e4effe04bec50aa Mon Sep 17 00:00:00 2001 From: Angelo Breuer Date: Sat, 9 Mar 2024 15:02:20 +0100 Subject: [PATCH 13/19] fix: Fix string-URI JSON converter write --- .../Converters/StringUriJsonConverter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Lavalink4NET.Integrations.LyricsJava/Converters/StringUriJsonConverter.cs b/src/Lavalink4NET.Integrations.LyricsJava/Converters/StringUriJsonConverter.cs index 2b8c640a..c3a1d4b2 100644 --- a/src/Lavalink4NET.Integrations.LyricsJava/Converters/StringUriJsonConverter.cs +++ b/src/Lavalink4NET.Integrations.LyricsJava/Converters/StringUriJsonConverter.cs @@ -12,6 +12,6 @@ public override Uri Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSeri public override void Write(Utf8JsonWriter writer, Uri value, JsonSerializerOptions options) { - writer.WriteString("url", value.ToString()); + writer.WriteStringValue(value.ToString()); } } From 80d0e1230a258c7838543f3062486b105078841f Mon Sep 17 00:00:00 2001 From: Angelo Breuer Date: Sat, 9 Mar 2024 15:59:20 +0100 Subject: [PATCH 14/19] feat: API model improvements --- .../Events/LyricsLoadedEventArgs.cs | 8 +- .../Extensions/LavalinkApiClientExtensions.cs | 115 ++++++++++++------ .../Lyrics.cs | 9 +- .../LyricsJavaIntegration.cs | 27 ++-- .../LyricsType.cs | 8 -- .../Models/LyricsResponseModel.cs | 23 ++-- .../Models/TextLyricsResponseModel.cs | 10 ++ .../Models/TimedLyricsResponseModel.cs | 11 ++ .../Players/ILavaLyricsPlayerListener.cs | 2 +- 9 files changed, 135 insertions(+), 78 deletions(-) delete mode 100644 src/Lavalink4NET.Integrations.LyricsJava/LyricsType.cs create mode 100644 src/Lavalink4NET.Integrations.LyricsJava/Models/TextLyricsResponseModel.cs create mode 100644 src/Lavalink4NET.Integrations.LyricsJava/Models/TimedLyricsResponseModel.cs diff --git a/src/Lavalink4NET.Integrations.LyricsJava/Events/LyricsLoadedEventArgs.cs b/src/Lavalink4NET.Integrations.LyricsJava/Events/LyricsLoadedEventArgs.cs index 242d83bb..c73e4fa3 100644 --- a/src/Lavalink4NET.Integrations.LyricsJava/Events/LyricsLoadedEventArgs.cs +++ b/src/Lavalink4NET.Integrations.LyricsJava/Events/LyricsLoadedEventArgs.cs @@ -2,13 +2,13 @@ public class LyricsLoadedEventArgs : EventArgs { - public LyricsLoadedEventArgs(ulong guildId, Lyrics lyrics) + public LyricsLoadedEventArgs(ulong guildId, Lyrics? lyrics) { GuildId = guildId; Lyrics = lyrics; } - + public ulong GuildId { get; } - - public Lyrics Lyrics { get; } + + public Lyrics? Lyrics { get; } } \ No newline at end of file diff --git a/src/Lavalink4NET.Integrations.LyricsJava/Extensions/LavalinkApiClientExtensions.cs b/src/Lavalink4NET.Integrations.LyricsJava/Extensions/LavalinkApiClientExtensions.cs index 4fcb7218..3e9a4bf8 100644 --- a/src/Lavalink4NET.Integrations.LyricsJava/Extensions/LavalinkApiClientExtensions.cs +++ b/src/Lavalink4NET.Integrations.LyricsJava/Extensions/LavalinkApiClientExtensions.cs @@ -1,14 +1,16 @@ namespace Lavalink4NET.Integrations.LyricsJava.Extensions; using System.Collections.Immutable; -using System.Net.Http.Json; +using System.Net; using System.Text.Json; +using System.Text.Json.Nodes; +using System.Text.Json.Serialization.Metadata; using Lavalink4NET.Integrations.LyricsJava.Models; using Lavalink4NET.Rest; public static class LavalinkApiClientExtensions { - public static async ValueTask GetCurrentTrackLyricsAsync( + public static async ValueTask GetCurrentTrackLyricsAsync( this ILavalinkApiClient apiClient, string sessionId, ulong guildId, @@ -25,18 +27,15 @@ public static async ValueTask GetCurrentTrackLyricsAsync( .GetAsync(endpoint, completionOption: HttpCompletionOption.ResponseHeadersRead, cancellationToken) .ConfigureAwait(false); - LyricsResponseModel? result; - try + if (response.StatusCode is HttpStatusCode.NotFound) { - result = await response.Content - .ReadFromJsonAsync(ModelJsonSerializerContext.Default.LyricsResponseModel, cancellationToken: cancellationToken) - .ConfigureAwait(false); - } - catch (JsonException) - { - result = null; + return null; } + var result = await response.Content + .ReadFromJsonWithWorkaroundAsync(ModelJsonSerializerContext.Default.LyricsResponseModel, cancellationToken: cancellationToken) + .ConfigureAwait(false); + return LyricsJavaIntegration.CreateLyrics(result); } @@ -55,24 +54,21 @@ public static async ValueTask> SearchLyricsAs .GetAsync(endpoint, completionOption: HttpCompletionOption.ResponseHeadersRead, cancellationToken) .ConfigureAwait(false); - ImmutableArray result; - try + if (response.StatusCode is HttpStatusCode.NotFound) { - result = await response.Content - .ReadFromJsonAsync(ModelJsonSerializerContext.Default.ImmutableArraySearchResultModel, cancellationToken: cancellationToken) - .ConfigureAwait(false); - } - catch (JsonException) - { - result = ImmutableArray.Empty; + return ImmutableArray.Empty; } + var result = await response.Content + .ReadFromJsonWithWorkaroundAsync(ModelJsonSerializerContext.Default.ImmutableArraySearchResultModel, cancellationToken: cancellationToken) + .ConfigureAwait(false); + return result .Select(x => new LyricsSearchResult(x.VideoId, x.Title)) .ToImmutableArray(); } - public static async ValueTask GetYouTubeLyricsAsync( + public static async ValueTask GetYouTubeLyricsAsync( this ILavalinkApiClient apiClient, string videoId, CancellationToken cancellationToken = default) @@ -87,22 +83,19 @@ public static async ValueTask GetYouTubeLyricsAsync( .GetAsync(endpoint, completionOption: HttpCompletionOption.ResponseHeadersRead, cancellationToken) .ConfigureAwait(false); - LyricsResponseModel? result; - try - { - result = await response.Content - .ReadFromJsonAsync(ModelJsonSerializerContext.Default.LyricsResponseModel, cancellationToken: cancellationToken) - .ConfigureAwait(false); - } - catch (JsonException) + if (response.StatusCode is HttpStatusCode.NotFound) { - result = null; + return null; } + var result = await response.Content + .ReadFromJsonWithWorkaroundAsync(ModelJsonSerializerContext.Default.LyricsResponseModel, cancellationToken: cancellationToken) + .ConfigureAwait(false); + return LyricsJavaIntegration.CreateLyrics(result); } - public static async ValueTask GetGeniusLyricsAsync( + public static async ValueTask GetGeniusLyricsAsync( this ILavalinkApiClient apiClient, string query, CancellationToken cancellationToken = default) @@ -117,18 +110,62 @@ public static async ValueTask GetGeniusLyricsAsync( .GetAsync(endpoint, completionOption: HttpCompletionOption.ResponseHeadersRead, cancellationToken) .ConfigureAwait(false); - LyricsResponseModel? result; - try + if (response.StatusCode is HttpStatusCode.NotFound) { - result = await response.Content - .ReadFromJsonAsync(cancellationToken: cancellationToken) - .ConfigureAwait(false); + return null; } - catch (JsonException) + + var result = await response.Content + .ReadFromJsonWithWorkaroundAsync(ModelJsonSerializerContext.Default.LyricsResponseModel, cancellationToken: cancellationToken) + .ConfigureAwait(false); + + return LyricsJavaIntegration.CreateLyrics(result); + } +} + +file static class LyricsJavaWorkaround +{ + public static async Task ReadFromJsonWithWorkaroundAsync(this HttpContent content, JsonTypeInfo jsonTypeInfo, CancellationToken cancellationToken = default) + { + // FIXME: LyricsJava somehow returns the type property twice in the object + cancellationToken.ThrowIfCancellationRequested(); + + T? Parse(ReadOnlySpan value) { - result = null; + var jsonObject = new JsonObject(); + var utf8JsonReader = new Utf8JsonReader(value); + var isFirst = true; + + if (!utf8JsonReader.Read()) + { + throw new JsonException("Unexpected EOF", new EndOfStreamException()); + } + + while (utf8JsonReader.Read() && utf8JsonReader.TokenType is not JsonTokenType.EndObject) + { + var propertyName = utf8JsonReader.GetString()!; + + if (!utf8JsonReader.Read()) + { + throw new JsonException("Unexpected EOF", new EndOfStreamException()); + } + + if (!isFirst && propertyName.Equals("type", StringComparison.Ordinal)) + { + continue; // Fix duplicate property + } + + jsonObject[propertyName] = JsonNode.Parse(ref utf8JsonReader); + isFirst = true; + } + + return jsonObject.Deserialize(jsonTypeInfo); } - return LyricsJavaIntegration.CreateLyrics(result); + var jsonObject = await content + .ReadAsByteArrayAsync(cancellationToken) + .ConfigureAwait(false); + + return Parse(jsonObject); } } \ No newline at end of file diff --git a/src/Lavalink4NET.Integrations.LyricsJava/Lyrics.cs b/src/Lavalink4NET.Integrations.LyricsJava/Lyrics.cs index 60fc2672..e8548bea 100644 --- a/src/Lavalink4NET.Integrations.LyricsJava/Lyrics.cs +++ b/src/Lavalink4NET.Integrations.LyricsJava/Lyrics.cs @@ -3,8 +3,7 @@ using System.Collections.Immutable; public sealed record class Lyrics( - LyricsType Type, - string? Source, - string? Basic, - LyricsTrack? Track, - ImmutableArray? Timed); \ No newline at end of file + string Source, + string Text, + LyricsTrack Track, + ImmutableArray? TimedLines); \ No newline at end of file diff --git a/src/Lavalink4NET.Integrations.LyricsJava/LyricsJavaIntegration.cs b/src/Lavalink4NET.Integrations.LyricsJava/LyricsJavaIntegration.cs index 7a7d3d10..f4c22593 100644 --- a/src/Lavalink4NET.Integrations.LyricsJava/LyricsJavaIntegration.cs +++ b/src/Lavalink4NET.Integrations.LyricsJava/LyricsJavaIntegration.cs @@ -67,16 +67,29 @@ await playerListener } } - internal static LyricsTrack CreateLyricsTrack(LyricsResponseTrackModel model) => new( + private static LyricsTrack CreateLyricsTrack(LyricsResponseTrackModel model) => new( Title: model.Title, Author: model.Author, Album: model.Album, AlbumArt: model.AlbumArt.Select(x => new AlbumArt(x.Url, x.Width, x.Height)).ToImmutableArray()); - internal static Lyrics CreateLyrics(LyricsResponseModel? model) => new( - Type: model?.Type == "timed" ? model.Type == "text" ? LyricsType.Basic : LyricsType.Timed : LyricsType.NotFound, - Source: model?.Source, - Basic: model?.LyricsText, - Track: model?.Track is not null ? CreateLyricsTrack(model.Track) : null, - Timed: model?.TimedLines?.Select(x => new TimedLyricsLine(x.Line, new TimeRange(x.Range.Start, x.Range.End))).ToImmutableArray()); + private static Lyrics CreateLyrics(TimedLyricsResponseModel model) => new( + Source: model.Source, + Text: string.Join("\n", model.TimedLines.Select(x => x.Line).Where(x => !string.IsNullOrWhiteSpace(x))), + Track: CreateLyricsTrack(model.Track), + TimedLines: model.TimedLines.Select(x => new TimedLyricsLine(x.Line, new TimeRange(x.Range.Start, x.Range.End))).ToImmutableArray()); + + private static Lyrics CreateLyrics(TextLyricsResponseModel model) => new( + Source: model.Source, + Text: model.LyricsText, + Track: CreateLyricsTrack(model.Track), + TimedLines: null); + + internal static Lyrics? CreateLyrics(LyricsResponseModel? model) => model switch + { + TimedLyricsResponseModel timedLyrics => CreateLyrics(timedLyrics), + TextLyricsResponseModel textLyrics => CreateLyrics(textLyrics), + null => null, + _ => throw new NotSupportedException(), + }; } \ No newline at end of file diff --git a/src/Lavalink4NET.Integrations.LyricsJava/LyricsType.cs b/src/Lavalink4NET.Integrations.LyricsJava/LyricsType.cs deleted file mode 100644 index 182ceaba..00000000 --- a/src/Lavalink4NET.Integrations.LyricsJava/LyricsType.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Lavalink4NET.Integrations.LyricsJava; - -public enum LyricsType -{ - NotFound, - Basic, - Timed -} \ No newline at end of file diff --git a/src/Lavalink4NET.Integrations.LyricsJava/Models/LyricsResponseModel.cs b/src/Lavalink4NET.Integrations.LyricsJava/Models/LyricsResponseModel.cs index cea3e155..28127c10 100644 --- a/src/Lavalink4NET.Integrations.LyricsJava/Models/LyricsResponseModel.cs +++ b/src/Lavalink4NET.Integrations.LyricsJava/Models/LyricsResponseModel.cs @@ -1,22 +1,17 @@ namespace Lavalink4NET.Integrations.LyricsJava.Models; -using System.Collections.Immutable; using System.Text.Json.Serialization; -public sealed record class LyricsResponseModel +[JsonPolymorphic(TypeDiscriminatorPropertyName = "type", UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FailSerialization)] +[JsonDerivedType(typeof(TextLyricsResponseModel), "text")] +[JsonDerivedType(typeof(TimedLyricsResponseModel), "timed")] +public abstract record class LyricsResponseModel { - [JsonPropertyName("type")] - public string Type { get; set; } = null!; - + [JsonRequired] [JsonPropertyName("track")] - public LyricsResponseTrackModel? Track { get; set; } - + public LyricsResponseTrackModel Track { get; set; } = null!; + + [JsonRequired] [JsonPropertyName("source")] public string Source { get; set; } = null!; - - [JsonPropertyName("text")] - public string? LyricsText { get; set; } - - [JsonPropertyName("lines")] - public ImmutableArray? TimedLines { get; set; } -} \ No newline at end of file +} diff --git a/src/Lavalink4NET.Integrations.LyricsJava/Models/TextLyricsResponseModel.cs b/src/Lavalink4NET.Integrations.LyricsJava/Models/TextLyricsResponseModel.cs new file mode 100644 index 00000000..97cd6bb9 --- /dev/null +++ b/src/Lavalink4NET.Integrations.LyricsJava/Models/TextLyricsResponseModel.cs @@ -0,0 +1,10 @@ +namespace Lavalink4NET.Integrations.LyricsJava.Models; + +using System.Text.Json.Serialization; + +public sealed record class TextLyricsResponseModel : LyricsResponseModel +{ + [JsonRequired] + [JsonPropertyName("text")] + public string LyricsText { get; set; } = null!; +} diff --git a/src/Lavalink4NET.Integrations.LyricsJava/Models/TimedLyricsResponseModel.cs b/src/Lavalink4NET.Integrations.LyricsJava/Models/TimedLyricsResponseModel.cs new file mode 100644 index 00000000..c9b83b5d --- /dev/null +++ b/src/Lavalink4NET.Integrations.LyricsJava/Models/TimedLyricsResponseModel.cs @@ -0,0 +1,11 @@ +namespace Lavalink4NET.Integrations.LyricsJava.Models; + +using System.Collections.Immutable; +using System.Text.Json.Serialization; + +public sealed record class TimedLyricsResponseModel : LyricsResponseModel +{ + [JsonRequired] + [JsonPropertyName("lines")] + public ImmutableArray TimedLines { get; set; } +} diff --git a/src/Lavalink4NET.Integrations.LyricsJava/Players/ILavaLyricsPlayerListener.cs b/src/Lavalink4NET.Integrations.LyricsJava/Players/ILavaLyricsPlayerListener.cs index 6546f901..78372797 100644 --- a/src/Lavalink4NET.Integrations.LyricsJava/Players/ILavaLyricsPlayerListener.cs +++ b/src/Lavalink4NET.Integrations.LyricsJava/Players/ILavaLyricsPlayerListener.cs @@ -4,5 +4,5 @@ public interface ILavaLyricsPlayerListener : ILavalinkPlayerListener { - ValueTask NotifyLyricsLoadedAsync(Lyrics lyrics, CancellationToken cancellationToken = default); + ValueTask NotifyLyricsLoadedAsync(Lyrics? lyrics, CancellationToken cancellationToken = default); } \ No newline at end of file From 5eebeac47b015b6845f28dd84e08a70aa875ffc7 Mon Sep 17 00:00:00 2001 From: Angelo Breuer Date: Sat, 9 Mar 2024 15:59:24 +0100 Subject: [PATCH 15/19] tests: Add tests for LyricsJava --- .../HttpClientFactory.cs | 77 +++++++++++++ ...k4NET.Integrations.LyricsJava.Tests.csproj | 4 +- .../LyricsJavaTests.cs | 107 ++++++++++++++++++ .../Properties/launchSettings.json | 12 ++ 4 files changed, 199 insertions(+), 1 deletion(-) create mode 100644 src/Lavalink4NET.Integrations.LyricsJava.Tests/HttpClientFactory.cs create mode 100644 src/Lavalink4NET.Integrations.LyricsJava.Tests/LyricsJavaTests.cs create mode 100644 src/Lavalink4NET.Integrations.LyricsJava.Tests/Properties/launchSettings.json diff --git a/src/Lavalink4NET.Integrations.LyricsJava.Tests/HttpClientFactory.cs b/src/Lavalink4NET.Integrations.LyricsJava.Tests/HttpClientFactory.cs new file mode 100644 index 00000000..74516791 --- /dev/null +++ b/src/Lavalink4NET.Integrations.LyricsJava.Tests/HttpClientFactory.cs @@ -0,0 +1,77 @@ +namespace Lavalink4NET.Rest.Tests; + +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Net.Http; +using System.Threading.Tasks; +using Microsoft.AspNetCore.TestHost; +using Microsoft.AspNetCore.WebSockets; + +[ExcludeFromCodeCoverage] +internal sealed class HttpClientFactory : IHttpClientFactory, IAsyncDisposable +{ + private int _state; // 0 = build, 1 = run, 2 = disposed + private readonly WebApplication _application; + + public HttpClientFactory() + { + var builder = WebApplication.CreateBuilder(); + builder.WebHost.UseTestServer(); + builder.Services.AddWebSockets(x => { }); + + _application = builder.Build(); + } + + public void Start() + { + var state = Interlocked.CompareExchange(ref _state, 1, 0); + ObjectDisposedException.ThrowIf(state is 2, this); + + if (state is 1) + { + throw new InvalidOperationException("The application is already running."); + } + + Debug.Assert(state is 0); + _ = _application.RunAsync(); + } + + public WebApplication Application + { + get + { + ObjectDisposedException.ThrowIf(_state is 2, this); + + if (_state is not 0) + { + throw new InvalidOperationException("The application can not be accessed after starting it."); + } + + return _application; + } + } + + public HttpClient CreateClient(string name) + { + ObjectDisposedException.ThrowIf(_state is 2, this); + + if (_state is not 1) + { + throw new InvalidOperationException("The application must be started before creating a client."); + } + + return _application.GetTestServer().CreateClient(); + } + + public ValueTask DisposeAsync() + { + var state = Interlocked.CompareExchange(ref _state, 2, 1); + + if (state is not 1) + { + return default; + } + + return _application.DisposeAsync(); + } +} diff --git a/src/Lavalink4NET.Integrations.LyricsJava.Tests/Lavalink4NET.Integrations.LyricsJava.Tests.csproj b/src/Lavalink4NET.Integrations.LyricsJava.Tests/Lavalink4NET.Integrations.LyricsJava.Tests.csproj index 2fd7a6b5..fe34cd3e 100644 --- a/src/Lavalink4NET.Integrations.LyricsJava.Tests/Lavalink4NET.Integrations.LyricsJava.Tests.csproj +++ b/src/Lavalink4NET.Integrations.LyricsJava.Tests/Lavalink4NET.Integrations.LyricsJava.Tests.csproj @@ -1,4 +1,4 @@ - + net7.0 @@ -10,7 +10,9 @@ + + runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/Lavalink4NET.Integrations.LyricsJava.Tests/LyricsJavaTests.cs b/src/Lavalink4NET.Integrations.LyricsJava.Tests/LyricsJavaTests.cs new file mode 100644 index 00000000..f9e14834 --- /dev/null +++ b/src/Lavalink4NET.Integrations.LyricsJava.Tests/LyricsJavaTests.cs @@ -0,0 +1,107 @@ +namespace Lavalink4NET.Integrations.LyricsJava.Tests; + +using System.Threading.Tasks; +using Lavalink4NET.Integrations.LyricsJava.Extensions; +using Lavalink4NET.Rest; +using Lavalink4NET.Rest.Tests; +using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Options; +using Moq; +using Xunit; + +public sealed class LyricsJavaTests +{ + [Fact] + public async Task TestGetGeniusLyricsAsync() + { + // Arrange + await using var httpClientFactory = new HttpClientFactory(); + + httpClientFactory.Application.MapGet("/v4/lyrics/search", (HttpContext context) => + { + Assert.Equal("Never Gonna Give You Up", context.Request.Query["query"].ToString()); + Assert.Equal("genius", context.Request.Query["source"].ToString()); + + return TypedResults.Text(""" + { + "type": "text", + "track": { + "title": "Never Gonna Give You Up", + "author": "Rick Astley", + "album": null, + "albumArt": [ + { + "url": "https://images.genius.com/88634fdafc60d4ff1e76944436c34a19.901x901x1.png", + "height": -1, + "width": -1 + } + ] + }, + "source": "genius.com", + "text": "[Intro]\nDesert you\nOoh-ooh-ooh-ooh\nHurt you\n\n[Verse 1]\nWe're no strangers to love\nYou know the rules and so do I (Do I)\nA full commitment's what I'm thinking of\nYou wouldn't get this from any other guy\n\n[Pre-Chorus]\nI just wanna tell you how I'm feeling\nGotta make you understand\n\n[Chorus]\nNever gonna give you up\nNever gonna let you down\nNever gonna run around and desert you\nNever gonna make you cry\nNever gonna say goodbye\nNever gonna tell a lie and hurt you\n\n[Verse 2]\nWe've known each other for so long\nYour heart's been aching, but you're too shy to say it (To say it)\nInside, we both know what's been going on (Going on)\nWe know the game, and we're gonna play it", + "type": "text" + } + """, + contentType: "application/json"); + }); + + httpClientFactory.Start(); + + var client = new LavalinkApiClient( + httpClientFactory: httpClientFactory, + options: Options.Create(new LavalinkApiClientOptions()), + memoryCache: Mock.Of(), + logger: NullLogger.Instance); + + // Act + var lyrics = await client + .GetGeniusLyricsAsync("Never Gonna Give You Up") + .ConfigureAwait(false); + + // Assert + Assert.NotNull(lyrics); + Assert.Contains("Desert you", lyrics.Text); + } + + [Fact] + public async Task TestGetYouTubeLyricsNotFoundAsync() + { + // Arrange + await using var httpClientFactory = new HttpClientFactory(); + + httpClientFactory.Application.MapGet("/v4/lyrics/search", (HttpContext context) => + { + Assert.Equal("Never Gonna Give You Up", context.Request.Query["query"].ToString()); + Assert.Empty(context.Request.Query["source"].ToString()); + + return TypedResults.Text(""" + { + "timestamp": 1709994651386, + "status": 404, + "error": "Not Found", + "message": "Lyrics were not found", + "path": "/v4/lyrics/fJ9rUzIMcZQ" + } + """, + contentType: "application/json", + statusCode: 404); + }); + + httpClientFactory.Start(); + + var client = new LavalinkApiClient( + httpClientFactory: httpClientFactory, + options: Options.Create(new LavalinkApiClientOptions()), + memoryCache: Mock.Of(), + logger: NullLogger.Instance); + + // Act + var lyrics = await client + .GetYouTubeLyricsAsync("dQw4w9WgXcQ") + .ConfigureAwait(false); + + // Assert + Assert.Null(lyrics); + } +} diff --git a/src/Lavalink4NET.Integrations.LyricsJava.Tests/Properties/launchSettings.json b/src/Lavalink4NET.Integrations.LyricsJava.Tests/Properties/launchSettings.json new file mode 100644 index 00000000..1b14049c --- /dev/null +++ b/src/Lavalink4NET.Integrations.LyricsJava.Tests/Properties/launchSettings.json @@ -0,0 +1,12 @@ +{ + "profiles": { + "Lavalink4NET.Integrations.LyricsJava.Tests": { + "commandName": "Project", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "https://localhost:52780;http://localhost:52781" + } + } +} \ No newline at end of file From 70d6ab6b326b4f76ff8af71f27ac00cc5a768052 Mon Sep 17 00:00:00 2001 From: Angelo Breuer Date: Sat, 9 Mar 2024 16:18:27 +0100 Subject: [PATCH 16/19] feat: Add JSON converter to null image dimensions if not present --- .../LyricsJavaTests.cs | 117 ++++++++++++++++++ .../AlbumArt.cs | 6 +- .../Converters/ImageDimensionJsonConverter.cs | 28 +++++ .../LyricsJavaIntegration.cs | 2 +- .../Models/AlbumArtModel.cs | 12 +- 5 files changed, 156 insertions(+), 9 deletions(-) create mode 100644 src/Lavalink4NET.Integrations.LyricsJava/Converters/ImageDimensionJsonConverter.cs diff --git a/src/Lavalink4NET.Integrations.LyricsJava.Tests/LyricsJavaTests.cs b/src/Lavalink4NET.Integrations.LyricsJava.Tests/LyricsJavaTests.cs index f9e14834..dc06826a 100644 --- a/src/Lavalink4NET.Integrations.LyricsJava.Tests/LyricsJavaTests.cs +++ b/src/Lavalink4NET.Integrations.LyricsJava.Tests/LyricsJavaTests.cs @@ -62,6 +62,123 @@ public async Task TestGetGeniusLyricsAsync() // Assert Assert.NotNull(lyrics); Assert.Contains("Desert you", lyrics.Text); + + var art = Assert.Single(lyrics.Track.AlbumArt); + Assert.Null(art.Width); + Assert.Null(art.Height); + Assert.Equal("https://images.genius.com/88634fdafc60d4ff1e76944436c34a19.901x901x1.png", art.Uri.ToString()); + } + + [Fact] + public async Task TestGetGeniusLyricsWithUnknownDimensionsAsync() + { + // Arrange + await using var httpClientFactory = new HttpClientFactory(); + + httpClientFactory.Application.MapGet("/v4/lyrics/search", (HttpContext context) => + { + Assert.Equal("Never Gonna Give You Up", context.Request.Query["query"].ToString()); + Assert.Equal("genius", context.Request.Query["source"].ToString()); + + return TypedResults.Text(""" + { + "type": "text", + "track": { + "title": "Never Gonna Give You Up", + "author": "Rick Astley", + "album": null, + "albumArt": [ + { + "url": "https://images.genius.com/88634fdafc60d4ff1e76944436c34a19.901x901x1.png", + "height": -1, + "width": -1 + } + ] + }, + "source": "genius.com", + "text": "[Intro]\nDesert you\nOoh-ooh-ooh-ooh\nHurt you\n\n[Verse 1]\nWe're no strangers to love\nYou know the rules and so do I (Do I)\nA full commitment's what I'm thinking of\nYou wouldn't get this from any other guy\n\n[Pre-Chorus]\nI just wanna tell you how I'm feeling\nGotta make you understand\n\n[Chorus]\nNever gonna give you up\nNever gonna let you down\nNever gonna run around and desert you\nNever gonna make you cry\nNever gonna say goodbye\nNever gonna tell a lie and hurt you\n\n[Verse 2]\nWe've known each other for so long\nYour heart's been aching, but you're too shy to say it (To say it)\nInside, we both know what's been going on (Going on)\nWe know the game, and we're gonna play it", + "type": "text" + } + """, + contentType: "application/json"); + }); + + httpClientFactory.Start(); + + var client = new LavalinkApiClient( + httpClientFactory: httpClientFactory, + options: Options.Create(new LavalinkApiClientOptions()), + memoryCache: Mock.Of(), + logger: NullLogger.Instance); + + // Act + var lyrics = await client + .GetGeniusLyricsAsync("Never Gonna Give You Up") + .ConfigureAwait(false); + + // Assert + Assert.NotNull(lyrics); + + var art = Assert.Single(lyrics.Track.AlbumArt); + Assert.Null(art.Width); + Assert.Null(art.Height); + Assert.Equal("https://images.genius.com/88634fdafc60d4ff1e76944436c34a19.901x901x1.png", art.Uri.ToString()); + } + + [Fact] + public async Task TestGetGeniusLyricsWithKnownDimensionsAsync() + { + // Arrange + await using var httpClientFactory = new HttpClientFactory(); + + httpClientFactory.Application.MapGet("/v4/lyrics/search", (HttpContext context) => + { + Assert.Equal("Never Gonna Give You Up", context.Request.Query["query"].ToString()); + Assert.Equal("genius", context.Request.Query["source"].ToString()); + + return TypedResults.Text(""" + { + "type": "text", + "track": { + "title": "Never Gonna Give You Up", + "author": "Rick Astley", + "album": null, + "albumArt": [ + { + "url": "https://images.genius.com/88634fdafc60d4ff1e76944436c34a19.901x901x1.png", + "height": 320, + "width": 480 + } + ] + }, + "source": "genius.com", + "text": "[Intro]\nDesert you\nOoh-ooh-ooh-ooh\nHurt you\n\n[Verse 1]\nWe're no strangers to love\nYou know the rules and so do I (Do I)\nA full commitment's what I'm thinking of\nYou wouldn't get this from any other guy\n\n[Pre-Chorus]\nI just wanna tell you how I'm feeling\nGotta make you understand\n\n[Chorus]\nNever gonna give you up\nNever gonna let you down\nNever gonna run around and desert you\nNever gonna make you cry\nNever gonna say goodbye\nNever gonna tell a lie and hurt you\n\n[Verse 2]\nWe've known each other for so long\nYour heart's been aching, but you're too shy to say it (To say it)\nInside, we both know what's been going on (Going on)\nWe know the game, and we're gonna play it", + "type": "text" + } + """, + contentType: "application/json"); + }); + + httpClientFactory.Start(); + + var client = new LavalinkApiClient( + httpClientFactory: httpClientFactory, + options: Options.Create(new LavalinkApiClientOptions()), + memoryCache: Mock.Of(), + logger: NullLogger.Instance); + + // Act + var lyrics = await client + .GetGeniusLyricsAsync("Never Gonna Give You Up") + .ConfigureAwait(false); + + // Assert + Assert.NotNull(lyrics); + + var art = Assert.Single(lyrics.Track.AlbumArt); + Assert.Equal(480, art.Width); + Assert.Equal(320, art.Height); + Assert.Equal("https://images.genius.com/88634fdafc60d4ff1e76944436c34a19.901x901x1.png", art.Uri.ToString()); } [Fact] diff --git a/src/Lavalink4NET.Integrations.LyricsJava/AlbumArt.cs b/src/Lavalink4NET.Integrations.LyricsJava/AlbumArt.cs index 7a227e39..e081b314 100644 --- a/src/Lavalink4NET.Integrations.LyricsJava/AlbumArt.cs +++ b/src/Lavalink4NET.Integrations.LyricsJava/AlbumArt.cs @@ -1,6 +1,6 @@ namespace Lavalink4NET.Integrations.LyricsJava; public sealed record class AlbumArt( - Uri Url, - int Width, - int Height); \ No newline at end of file + Uri Uri, + int? Width, + int? Height); \ No newline at end of file diff --git a/src/Lavalink4NET.Integrations.LyricsJava/Converters/ImageDimensionJsonConverter.cs b/src/Lavalink4NET.Integrations.LyricsJava/Converters/ImageDimensionJsonConverter.cs new file mode 100644 index 00000000..1e56a1e2 --- /dev/null +++ b/src/Lavalink4NET.Integrations.LyricsJava/Converters/ImageDimensionJsonConverter.cs @@ -0,0 +1,28 @@ +namespace Lavalink4NET.Integrations.LyricsJava.Converters; + +using System; +using System.Text.Json; +using System.Text.Json.Serialization; + +public sealed class ImageDimensionJsonConverter : JsonConverter +{ + public override bool HandleNull => true; + + public override int? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + var value = reader.GetInt32(); + return value < 0 ? null : value; + } + + public override void Write(Utf8JsonWriter writer, int? value, JsonSerializerOptions options) + { + if (value.HasValue) + { + writer.WriteNumberValue(value.Value); + } + else + { + writer.WriteNullValue(); + } + } +} diff --git a/src/Lavalink4NET.Integrations.LyricsJava/LyricsJavaIntegration.cs b/src/Lavalink4NET.Integrations.LyricsJava/LyricsJavaIntegration.cs index f4c22593..b837366a 100644 --- a/src/Lavalink4NET.Integrations.LyricsJava/LyricsJavaIntegration.cs +++ b/src/Lavalink4NET.Integrations.LyricsJava/LyricsJavaIntegration.cs @@ -71,7 +71,7 @@ await playerListener Title: model.Title, Author: model.Author, Album: model.Album, - AlbumArt: model.AlbumArt.Select(x => new AlbumArt(x.Url, x.Width, x.Height)).ToImmutableArray()); + AlbumArt: model.AlbumArt.Select(x => new AlbumArt(x.Uri, x.Width, x.Height)).ToImmutableArray()); private static Lyrics CreateLyrics(TimedLyricsResponseModel model) => new( Source: model.Source, diff --git a/src/Lavalink4NET.Integrations.LyricsJava/Models/AlbumArtModel.cs b/src/Lavalink4NET.Integrations.LyricsJava/Models/AlbumArtModel.cs index c5c1793a..356d6a6f 100644 --- a/src/Lavalink4NET.Integrations.LyricsJava/Models/AlbumArtModel.cs +++ b/src/Lavalink4NET.Integrations.LyricsJava/Models/AlbumArtModel.cs @@ -7,11 +7,13 @@ public sealed record class AlbumArtModel { [JsonPropertyName("url")] [JsonConverter(typeof(StringUriJsonConverter))] - public Uri Url { get; set; } = null!; - + public Uri Uri { get; set; } = null!; + [JsonPropertyName("height")] - public int Height { get; set; } - + [JsonConverter(typeof(ImageDimensionJsonConverter))] + public int? Height { get; set; } + [JsonPropertyName("width")] - public int Width { get; set; } + [JsonConverter(typeof(ImageDimensionJsonConverter))] + public int? Width { get; set; } } \ No newline at end of file From d39694f2f3eec72b4a0befc255bf99e4c1042d55 Mon Sep 17 00:00:00 2001 From: Angelo Breuer Date: Sat, 9 Mar 2024 16:24:46 +0100 Subject: [PATCH 17/19] chore: Update package description --- .../Lavalink4NET.Integrations.LyricsJava.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Lavalink4NET.Integrations.LyricsJava/Lavalink4NET.Integrations.LyricsJava.csproj b/src/Lavalink4NET.Integrations.LyricsJava/Lavalink4NET.Integrations.LyricsJava.csproj index 5e1869ba..a7074c3b 100644 --- a/src/Lavalink4NET.Integrations.LyricsJava/Lavalink4NET.Integrations.LyricsJava.csproj +++ b/src/Lavalink4NET.Integrations.LyricsJava/Lavalink4NET.Integrations.LyricsJava.csproj @@ -6,7 +6,7 @@ enable - High performance Lavalink wrapper for .NET | Expand your audio playback experience with adding support for the Lavasrc audio source manager which adds support for searching tracks on Spotify, Apple Music, Deezer, Yandex Music and Flowery TTS. + Fetch timed lyrics from youtube or non-timed lyrics from genius. Automatically fetches lyrics for the current track. Requires the installation of the corresponding plugin on the Lavalink node. From 4873a18c96f25382cd55f0017da8014ebf23f0da Mon Sep 17 00:00:00 2001 From: Angelo Breuer Date: Sat, 9 Mar 2024 16:30:55 +0100 Subject: [PATCH 18/19] chore: Update docs --- docs/docs/integrations/lyricsjava.md | 26 +++++++------------------- 1 file changed, 7 insertions(+), 19 deletions(-) diff --git a/docs/docs/integrations/lyricsjava.md b/docs/docs/integrations/lyricsjava.md index 9e1467d4..90bb160d 100644 --- a/docs/docs/integrations/lyricsjava.md +++ b/docs/docs/integrations/lyricsjava.md @@ -31,16 +31,12 @@ That's it! The LyricsJava plugin is now integrated with Lavalink4NET. For getting the lyrics of the current track, you can use the `GetCurrentTrackLyricsAsync` method. This method will return the lyrics of the current track. The method requires the session id and the guild id both of which you can get from player properties. ```csharp -var apiClient = await AudioService.ApiClientProvider - .GetClientAsync() - .ConfigureAwait(false); - var player = await audioService.Players .GetPlayerAsync(guildId) .ConfigureAwait(false); -var lyrics = await apiClient - .GetCurrentTrackLyricsAsync(player.SessionId, player.GuildId) +var lyrics = await audioService.Tracks + .GetCurrentTrackLyricsAsync(player) .ConfigureAwait(false); ``` @@ -49,18 +45,14 @@ var lyrics = await apiClient For getting the lyrics of a youtube video, you can use the `GetYoutubeLyricsAsync` method. This method will return the lyrics of the youtube video. The method requires a youtube video id, which can be acquired by using the `SearchAsync` method if using a different provider (e.g. Spotify). ```csharp -var apiClient = await AudioService.ApiClientProvider - .GetClientAsync() - .ConfigureAwait(false); - -var results = await apiClient - .SearchAsync("Queen - Bohemian Rhapsody") +var results = await AudioService.Tracks + .SearchLyricsAsync("Queen - Bohemian Rhapsody") .ConfigureAwait(false); var videoId = results.First().VideoId; -var lyrics = await apiClient - .GetYoutubeLyricsAsync(videoId) // Youtube Video Id (e.g. dQw4w9WgXcQ) +var lyrics = await AudioService.Tracks + .GetYouTubeLyricsAsync(videoId) // Youtube Video Id (e.g. dQw4w9WgXcQ) .ConfigureAwait(false); ``` @@ -69,11 +61,7 @@ var lyrics = await apiClient For getting the lyrics of a song from genius, you can use the `GetGeniusLyricsAsync` method. This method will return the lyrics of the song. The method requires the song name and the artist name. ```csharp -var apiClient = await AudioService.ApiClientProvider - .GetClientAsync() - .ConfigureAwait(false); - -var lyrics = await apiClient +var lyrics = await AudioService.Tracks .GetGeniusLyricsAsync("Queen - Bohemian Rhapsody") .ConfigureAwait(false); ``` From a3fd91bd0b689bfb86439527f95e7d0261212778 Mon Sep 17 00:00:00 2001 From: Angelo Breuer Date: Sat, 9 Mar 2024 16:31:00 +0100 Subject: [PATCH 19/19] fix: Fix missing nullable annotations --- .../Extensions/TrackManagerExtensions.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Lavalink4NET.Integrations.LyricsJava/Extensions/TrackManagerExtensions.cs b/src/Lavalink4NET.Integrations.LyricsJava/Extensions/TrackManagerExtensions.cs index 91576b56..fa498550 100644 --- a/src/Lavalink4NET.Integrations.LyricsJava/Extensions/TrackManagerExtensions.cs +++ b/src/Lavalink4NET.Integrations.LyricsJava/Extensions/TrackManagerExtensions.cs @@ -7,7 +7,7 @@ public static class TrackManagerExtensions { - public static async ValueTask GetCurrentTrackLyricsAsync( + public static async ValueTask GetCurrentTrackLyricsAsync( this ITrackManager trackManager, ILavalinkPlayer player, CancellationToken cancellationToken = default) @@ -45,7 +45,7 @@ public static async ValueTask> SearchLyricsAs .ConfigureAwait(false); } - public static async ValueTask GetYouTubeLyricsAsync( + public static async ValueTask GetYouTubeLyricsAsync( this ITrackManager trackManager, string query, CancellationToken cancellationToken = default) @@ -64,7 +64,7 @@ public static async ValueTask GetYouTubeLyricsAsync( .ConfigureAwait(false); } - public static async ValueTask GetGeniusLyricsAsync( + public static async ValueTask GetGeniusLyricsAsync( this ITrackManager trackManager, string query, CancellationToken cancellationToken = default)