From 59a206e64bbde49726a0e3740bb8499630e8c8f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=92=D0=B8=D1=82=D0=B0=D0=BB=D0=B8=D0=B9=20=D0=9A=D0=B2?= =?UTF-8?q?=D0=B0=D1=88=D0=B8=D0=BD?= Date: Sun, 30 Aug 2026 19:31:32 +0200 Subject: [PATCH 01/11] #52 Updated packages, fixed warnings --- .../LyricsScraperNET.Client.csproj | 16 ++++----- .../ServiceCollectionExtensions.cs | 6 ++++ LyricsScraperNET/LyricsScraperNET.csproj | 21 ++++++----- .../Abstract/ExternalProviderBase.cs | 36 +++++++++---------- .../Providers/Genius/GeniusOptions.cs | 2 +- .../Providers/Genius/GeniusProvider.cs | 4 +-- .../Providers/KPopLyrics/KPopLyricsParser.cs | 2 +- .../KPopLyrics/KPopLyricsProvider.cs | 8 ++--- .../Musixmatch/MusixmatchTokenCache.cs | 30 +++++++--------- .../LyricsScraperClientTests.cs | 8 ++--- .../LyricsScraperNET.IntegrationTest.csproj | 18 +++++----- .../AZLyrics/AZLyricsProviderTest.cs | 4 +-- .../Providers/Genius/GeniusProviderTest.cs | 18 +++++----- .../KPopLyrics/KPopLyricsProviderTest.cs | 18 +++------- .../LyricFind/LyricFindProviderTest.cs | 16 ++++----- .../LyricsFreak/LyricsFreakProviderTest.cs | 4 +-- .../Musixmatch/MusixmatchProviderTest.cs | 14 ++++---- .../SongLyrics/SongLyricsProviderTest.cs | 10 +++--- .../LyricsScraperNET.TestShared.csproj | 7 ++-- .../LyricsScraperClientTests.cs | 28 ++++++++------- .../LyricsScraperNET.UnitTest.csproj | 32 ++++++----------- .../AZLyrics/AZLyricsProviderTest.cs | 2 +- .../Providers/Genius/GeniusProviderTest.cs | 4 +-- .../KPopLyrics/KPopLyricsProviderTest.cs | 4 +-- .../LyricFind/LyricFindProviderTest.cs | 12 +++---- .../LyricsFreak/LyricsFreakProviderTest.cs | 2 +- .../SongLyrics/SongLyricsProviderTest.cs | 4 +-- 27 files changed, 153 insertions(+), 177 deletions(-) diff --git a/LyricsScraperNET.Client/LyricsScraperNET.Client.csproj b/LyricsScraperNET.Client/LyricsScraperNET.Client.csproj index 74cd85b..7b27ced 100644 --- a/LyricsScraperNET.Client/LyricsScraperNET.Client.csproj +++ b/LyricsScraperNET.Client/LyricsScraperNET.Client.csproj @@ -2,18 +2,18 @@ Exe - net8.0;net7.0;net6.0;net5.0;netcoreapp3.1 + net10.0;net9.0;net8.0 enable - - - - - - - + + + + + + + diff --git a/LyricsScraperNET/Configuration/ServiceCollectionExtensions.cs b/LyricsScraperNET/Configuration/ServiceCollectionExtensions.cs index 0b7bad4..e359bc8 100644 --- a/LyricsScraperNET/Configuration/ServiceCollectionExtensions.cs +++ b/LyricsScraperNET/Configuration/ServiceCollectionExtensions.cs @@ -13,8 +13,14 @@ namespace LyricsScraperNET.Configuration { + /// + /// Extension methods for configuring the LyricsScraperNET services in the DI container. + /// public static class ServiceCollectionExtensions { + /// + /// Adds the LyricsScraperNET services to the service collection with automatic provider discovery. + /// public static IServiceCollection AddLyricScraperClientService( this IServiceCollection services, IConfiguration configuration) diff --git a/LyricsScraperNET/LyricsScraperNET.csproj b/LyricsScraperNET/LyricsScraperNET.csproj index 7d3c017..20eaab9 100644 --- a/LyricsScraperNET/LyricsScraperNET.csproj +++ b/LyricsScraperNET/LyricsScraperNET.csproj @@ -1,8 +1,8 @@  - net8.0;net7.0;net6.0;net5.0;netstandard2.1;netstandard2.0 - 9.0 + net10.0;net9.0;net8.0 + latest enable LyricsScraperNET LyricsScraperNET @@ -28,16 +28,15 @@ - - - - - - - - + + + + + + + + - diff --git a/LyricsScraperNET/Providers/Abstract/ExternalProviderBase.cs b/LyricsScraperNET/Providers/Abstract/ExternalProviderBase.cs index 6534d30..be670c0 100644 --- a/LyricsScraperNET/Providers/Abstract/ExternalProviderBase.cs +++ b/LyricsScraperNET/Providers/Abstract/ExternalProviderBase.cs @@ -27,15 +27,14 @@ public virtual SearchResult SearchLyric(SearchRequest searchRequest, Cancellatio if (!IsEnabled) return new SearchResult(); - switch (searchRequest) + return searchRequest switch { - case ArtistAndSongSearchRequest artistAndSongSearchRequest: - return SearchLyric(artistAndSongSearchRequest.Artist, artistAndSongSearchRequest.Song, cancellationToken); - case UriSearchRequest uriSearchRequest: - return SearchLyric(uriSearchRequest.Uri, cancellationToken); - default: - return new SearchResult(); - } + ArtistAndSongSearchRequest artistAndSongSearchRequest + => SearchLyric(artistAndSongSearchRequest.Artist, artistAndSongSearchRequest.Song, cancellationToken), + UriSearchRequest uriSearchRequest + => SearchLyric(uriSearchRequest.Uri, cancellationToken), + _ => new SearchResult(), + }; } protected virtual SearchResult SearchLyric(Uri uri, CancellationToken cancellationToken = default) @@ -53,15 +52,14 @@ public virtual async Task SearchLyricAsync(SearchRequest searchReq if (!IsEnabled) return new SearchResult(); - switch (searchRequest) + return searchRequest switch { - case ArtistAndSongSearchRequest artistAndSongSearchRequest: - return await SearchLyricAsync(artistAndSongSearchRequest.Artist, artistAndSongSearchRequest.Song, cancellationToken); - case UriSearchRequest uriSearchRequest: - return await SearchLyricAsync(uriSearchRequest.Uri, cancellationToken); - default: - return new SearchResult(); - } + ArtistAndSongSearchRequest artistAndSongSearchRequest + => await SearchLyricAsync(artistAndSongSearchRequest.Artist, artistAndSongSearchRequest.Song, cancellationToken), + UriSearchRequest uriSearchRequest + => await SearchLyricAsync(uriSearchRequest.Uri, cancellationToken), + _ => new SearchResult(), + }; } protected virtual Task SearchLyricAsync(Uri uri, CancellationToken cancellationToken = default) @@ -86,14 +84,12 @@ public void WithWebClient(IWebClient webClient) public void Enable() { - if (Options != null) - Options.Enabled = true; + Options?.Enabled = true; } public void Disable() { - if (Options != null) - Options.Enabled = false; + Options?.Enabled = false; } public virtual void WithLogger(ILoggerFactory loggerFactory) diff --git a/LyricsScraperNET/Providers/Genius/GeniusOptions.cs b/LyricsScraperNET/Providers/Genius/GeniusOptions.cs index d37d292..c57ff62 100644 --- a/LyricsScraperNET/Providers/Genius/GeniusOptions.cs +++ b/LyricsScraperNET/Providers/Genius/GeniusOptions.cs @@ -9,7 +9,7 @@ public sealed class GeniusOptions : IExternalProviderOptionsWithApiKey public bool Enabled { get; set; } // Optional. Use to retrieve lyric url for provided artist and song. - public string ApiKey { get; set; } + public string ApiKey { get; set; } = string.Empty; public string ConfigurationSectionName { get; } = "GeniusOptions"; diff --git a/LyricsScraperNET/Providers/Genius/GeniusProvider.cs b/LyricsScraperNET/Providers/Genius/GeniusProvider.cs index 6b66df9..44ec2cd 100644 --- a/LyricsScraperNET/Providers/Genius/GeniusProvider.cs +++ b/LyricsScraperNET/Providers/Genius/GeniusProvider.cs @@ -176,7 +176,7 @@ private string GetParsedLyricFromHtmlPageBody(string htmlPageBody, out bool inst var referentFragmentNodes = htmlDocument.DocumentNode.SelectNodes(_referentFragmentNodesXPath); if (referentFragmentNodes != null) foreach (HtmlNode fragmentNode in referentFragmentNodes) - fragmentNode.ParentNode.ReplaceChild(htmlDocument.CreateTextNode(fragmentNode.ChildNodes[0].InnerHtml), fragmentNode); + fragmentNode.ParentNode?.ReplaceChild(htmlDocument.CreateTextNode(fragmentNode.ChildNodes[0].InnerHtml), fragmentNode); var spanNodes = htmlDocument.DocumentNode.SelectNodes("//span"); if (spanNodes != null) foreach (HtmlNode spanNode in spanNodes) @@ -227,7 +227,7 @@ private string GetLyricUrlFromSearchResponse(SearchResponse searchResponse, stri return artistAndSongHit.Result.Url; } - private string GetApiSearchQuery(string artist, string song) + private static string GetApiSearchQuery(string artist, string song) => string.Format(GeniusSearchQueryFormat, artist, song); } } diff --git a/LyricsScraperNET/Providers/KPopLyrics/KPopLyricsParser.cs b/LyricsScraperNET/Providers/KPopLyrics/KPopLyricsParser.cs index 6136d8f..f3e44d1 100644 --- a/LyricsScraperNET/Providers/KPopLyrics/KPopLyricsParser.cs +++ b/LyricsScraperNET/Providers/KPopLyrics/KPopLyricsParser.cs @@ -13,7 +13,7 @@ public string Parse(string lyric) htmlDoc.LoadHtml(lyric); var deEntitizedText = string.Join("\n\n", //

-> \n\n - htmlDoc.DocumentNode.SelectNodes("//p") + htmlDoc!.DocumentNode!.SelectNodes("//p")! .Select(node => HtmlEntity.DeEntitize(node.InnerHtml .Replace("
", "\n") // the trailing whitespace after
is necessary .Trim() diff --git a/LyricsScraperNET/Providers/KPopLyrics/KPopLyricsProvider.cs b/LyricsScraperNET/Providers/KPopLyrics/KPopLyricsProvider.cs index 8901e9e..db02ba0 100644 --- a/LyricsScraperNET/Providers/KPopLyrics/KPopLyricsProvider.cs +++ b/LyricsScraperNET/Providers/KPopLyrics/KPopLyricsProvider.cs @@ -119,7 +119,7 @@ private SearchResult PostProcessLyric(Uri uri, string text) var htmlDoc = new HtmlDocument(); htmlDoc.LoadHtml(text); - var mainNode = htmlDoc.DocumentNode.SelectNodes(LyricsContainerNodesXPath).FirstOrDefault(); + var mainNode = htmlDoc.DocumentNode?.SelectNodes(LyricsContainerNodesXPath)?.FirstOrDefault(); if (mainNode is null) { @@ -133,9 +133,9 @@ private SearchResult PostProcessLyric(Uri uri, string text) return new SearchResult(ExternalProviderType.KPopLyrics, ResponseStatusCode.NoDataFound); } - var h2Nodes = htmlDoc.DocumentNode.SelectNodes("//h2"); + var h2Nodes = htmlDoc.DocumentNode?.SelectNodes("//h2"); - if (h2Nodes is null || !h2Nodes.Any()) + if (h2Nodes is null || h2Nodes.Count == 0) { _logger?.LogWarning($"KPopLyrics. Can't parse lyric from the page. Couldn't find header nodes. Uri: {uri}"); return new SearchResult(ExternalProviderType.KPopLyrics, ResponseStatusCode.NoDataFound); @@ -167,7 +167,7 @@ private SearchResult PostProcessLyric(Uri uri, string text) return new SearchResult(result, ExternalProviderType.KPopLyrics); } - private string TakeParagraphsUntilHeader(HtmlNode startNode) + private static string TakeParagraphsUntilHeader(HtmlNode startNode) { var paragraphs = new List(); diff --git a/LyricsScraperNET/Providers/Musixmatch/MusixmatchTokenCache.cs b/LyricsScraperNET/Providers/Musixmatch/MusixmatchTokenCache.cs index 6158997..e8cd49b 100644 --- a/LyricsScraperNET/Providers/Musixmatch/MusixmatchTokenCache.cs +++ b/LyricsScraperNET/Providers/Musixmatch/MusixmatchTokenCache.cs @@ -9,10 +9,10 @@ public sealed class MusixmatchTokenCache : IMusixmatchTokenCache private ILogger? _logger; // Musixmatch Token memory cache - private static IMemoryCache? _memoryCache; + private static MemoryCache? _memoryCache; private static MemoryCacheEntryOptions? _memoryCacheEntryOptions; - private static readonly object _syncLock = new object(); + private static readonly object _syncLock = new(); private readonly string MusixmatchTokenKey = "MusixmatchToken"; @@ -22,36 +22,30 @@ public MusixmatchTokenCache() InitializeMemoryCacheEntryOptions(); } - private void InitializeMemoryCache() + private static void InitializeMemoryCache() { if (_memoryCache == null) { lock (_syncLock) { - if (_memoryCache == null) + _memoryCache ??= new MemoryCache(new MemoryCacheOptions() { - _memoryCache = new MemoryCache(new MemoryCacheOptions() - { - SizeLimit = 1024, - }); - } + SizeLimit = 1024, + }); } } } - private void InitializeMemoryCacheEntryOptions() + private static void InitializeMemoryCacheEntryOptions() { if (_memoryCacheEntryOptions == null) { lock (_syncLock) { - if (_memoryCacheEntryOptions == null) + _memoryCacheEntryOptions ??= new MemoryCacheEntryOptions() { - _memoryCacheEntryOptions = new MemoryCacheEntryOptions() - { - Size = 1 - }; - } + Size = 1 + }; } } } @@ -68,12 +62,12 @@ public string GetOrCreateToken(bool regenerate = false) _logger?.LogDebug("Musixmatch. Use default MusixmatchToken."); string musixmatchTokenValue; - if (!_memoryCache.TryGetValue(MusixmatchTokenKey, out musixmatchTokenValue)) + if (!_memoryCache!.TryGetValue(MusixmatchTokenKey, out musixmatchTokenValue!)) { _logger?.LogDebug("Musixmatch. Generate new token."); var musixmatchToken = new MusixmatchToken(); musixmatchTokenValue = musixmatchToken.Token; - _memoryCache.Set(MusixmatchTokenKey, musixmatchTokenValue, _memoryCacheEntryOptions); + _memoryCache!.Set(MusixmatchTokenKey, musixmatchTokenValue, _memoryCacheEntryOptions); } return musixmatchTokenValue; } diff --git a/Tests/LyricsScraperNET.IntegrationTest/LyricsScraperClientTests.cs b/Tests/LyricsScraperNET.IntegrationTest/LyricsScraperClientTests.cs index 6b9565c..7f77dd3 100644 --- a/Tests/LyricsScraperNET.IntegrationTest/LyricsScraperClientTests.cs +++ b/Tests/LyricsScraperNET.IntegrationTest/LyricsScraperClientTests.cs @@ -20,7 +20,7 @@ ILyricsScraperClient lyricsScraperClient var searchRequest = new ArtistAndSongSearchRequest(artistToSearch, songToSearch); // Act - var searchResult = lyricsScraperClient.SearchLyric(searchRequest); + var searchResult = lyricsScraperClient.SearchLyric(searchRequest, TestContext.Current.CancellationToken); // Assert Assert.NotNull(searchResult); @@ -44,7 +44,7 @@ ILyricsScraperClient lyricsScraperClient var searchRequest = new ArtistAndSongSearchRequest(artistToSearch, songToSearch); // Act - var searchResult = await lyricsScraperClient.SearchLyricAsync(searchRequest); + var searchResult = await lyricsScraperClient.SearchLyricAsync(searchRequest, TestContext.Current.CancellationToken); // Assert Assert.NotNull(searchResult); @@ -68,7 +68,7 @@ ILyricsScraperClient lyricsScraperClient var searchRequest = new ArtistAndSongSearchRequest(artistToSearch, songToSearch); // Act - var searchResult = lyricsScraperClient.SearchLyric(searchRequest); + var searchResult = lyricsScraperClient.SearchLyric(searchRequest, TestContext.Current.CancellationToken); // Assert Assert.NotNull(searchResult); @@ -91,7 +91,7 @@ ILyricsScraperClient lyricsScraperClient var searchRequest = new ArtistAndSongSearchRequest(artistToSearch, songToSearch); // Act - var searchResult = await lyricsScraperClient.SearchLyricAsync(searchRequest); + var searchResult = await lyricsScraperClient.SearchLyricAsync(searchRequest, TestContext.Current.CancellationToken); // Assert Assert.NotNull(searchResult); diff --git a/Tests/LyricsScraperNET.IntegrationTest/LyricsScraperNET.IntegrationTest.csproj b/Tests/LyricsScraperNET.IntegrationTest/LyricsScraperNET.IntegrationTest.csproj index b12cd25..86d9133 100644 --- a/Tests/LyricsScraperNET.IntegrationTest/LyricsScraperNET.IntegrationTest.csproj +++ b/Tests/LyricsScraperNET.IntegrationTest/LyricsScraperNET.IntegrationTest.csproj @@ -5,25 +5,23 @@ - net8.0 - 9.0 + net10.0 + latest false - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/Tests/LyricsScraperNET.IntegrationTest/Providers/AZLyrics/AZLyricsProviderTest.cs b/Tests/LyricsScraperNET.IntegrationTest/Providers/AZLyrics/AZLyricsProviderTest.cs index 973b8ab..8a56069 100644 --- a/Tests/LyricsScraperNET.IntegrationTest/Providers/AZLyrics/AZLyricsProviderTest.cs +++ b/Tests/LyricsScraperNET.IntegrationTest/Providers/AZLyrics/AZLyricsProviderTest.cs @@ -15,7 +15,7 @@ public class AZLyricsProviderTest : ProviderTestBase #region sync [Theory] - [MemberData(nameof(GetTestData), parameters: "Providers\\AZLyrics\\lyric_test_data.json")] + [MemberData(nameof(GetTestData), arguments: "Providers\\AZLyrics\\lyric_test_data.json")] public void SearchLyric_IntegrationDynamicData_Success(LyricsTestData testData) { // Arrange @@ -61,7 +61,7 @@ public void SearchLyric_NotExistsLyrics_ShouldReturnNoDataFoundStatus(string art #region async [Theory] - [MemberData(nameof(GetTestData), parameters: "Providers\\AZLyrics\\lyric_test_data.json")] + [MemberData(nameof(GetTestData), arguments: "Providers\\AZLyrics\\lyric_test_data.json")] public async Task SearchLyricAsync_IntegrationDynamicData_Success(LyricsTestData testData) { // Arrange diff --git a/Tests/LyricsScraperNET.IntegrationTest/Providers/Genius/GeniusProviderTest.cs b/Tests/LyricsScraperNET.IntegrationTest/Providers/Genius/GeniusProviderTest.cs index f1e2f92..83c7b94 100644 --- a/Tests/LyricsScraperNET.IntegrationTest/Providers/Genius/GeniusProviderTest.cs +++ b/Tests/LyricsScraperNET.IntegrationTest/Providers/Genius/GeniusProviderTest.cs @@ -15,7 +15,7 @@ public class GeniusProviderTest : ProviderTestBase #region sync [Theory] - [MemberData(nameof(GetTestData), parameters: "Providers\\Genius\\lyric_test_data.json")] + [MemberData(nameof(GetTestData), arguments: "Providers\\Genius\\lyric_test_data.json")] public void SearchLyric_IntegrationDynamicData_Success(LyricsTestData testData) { // Arrange @@ -37,7 +37,7 @@ public void SearchLyric_IntegrationDynamicData_Success(LyricsTestData testData) } [Theory] - [MemberData(nameof(GetTestData), parameters: "Providers\\Genius\\instrumental_test_data.json")] + [MemberData(nameof(GetTestData), arguments: "Providers\\Genius\\instrumental_test_data.json")] public void SearchLyric_UnitDynamicData_Instrumental(LyricsTestData testData) { // Arrange @@ -58,7 +58,7 @@ public void SearchLyric_UnitDynamicData_Instrumental(LyricsTestData testData) } [Theory] - [InlineData("asdfasdfasdfasdf", "asdfasdfasdfasdf")] + [InlineData("asdfasdfasdfasdf123321", "asdfasdfasdfasdf123321")] public void SearchLyric_NotExistsLyrics_ShouldReturnNoDataFoundStatus(string artist, string song) { // Arrange @@ -83,7 +83,7 @@ public void SearchLyric_NotExistsLyrics_ShouldReturnNoDataFoundStatus(string art #region async [Theory] - [MemberData(nameof(GetTestData), parameters: "Providers\\Genius\\lyric_test_data.json")] + [MemberData(nameof(GetTestData), arguments: "Providers\\Genius\\lyric_test_data.json")] public async Task SearchLyricAsync_IntegrationDynamicData_Success(LyricsTestData testData) { // Arrange @@ -91,7 +91,7 @@ public async Task SearchLyricAsync_IntegrationDynamicData_Success(LyricsTestData SearchRequest searchRequest = CreateSearchRequest(testData); // Act - var searchResult = await lyricsClient.SearchLyricAsync(searchRequest); + var searchResult = await lyricsClient.SearchLyricAsync(searchRequest, TestContext.Current.CancellationToken); // Assert Assert.NotNull(searchResult); @@ -104,7 +104,7 @@ public async Task SearchLyricAsync_IntegrationDynamicData_Success(LyricsTestData } [Theory] - [MemberData(nameof(GetTestData), parameters: "Providers\\Genius\\instrumental_test_data.json")] + [MemberData(nameof(GetTestData), arguments: "Providers\\Genius\\instrumental_test_data.json")] public async Task SearchLyricAsync_UnitDynamicData_Instrumental(LyricsTestData testData) { // Arrange @@ -112,7 +112,7 @@ public async Task SearchLyricAsync_UnitDynamicData_Instrumental(LyricsTestData t SearchRequest searchRequest = CreateSearchRequest(testData); // Act - var searchResult = await lyricsClient.SearchLyricAsync(searchRequest); + var searchResult = await lyricsClient.SearchLyricAsync(searchRequest, TestContext.Current.CancellationToken); // Assert Assert.NotNull(searchResult); @@ -124,7 +124,7 @@ public async Task SearchLyricAsync_UnitDynamicData_Instrumental(LyricsTestData t } [Theory] - [InlineData("asdfasdfasdfasdf", "asdfasdfasdfasdf")] + [InlineData("asdfasdfasdfasdf123321", "asdfasdfasdfasdf123321")] public async Task SearchLyricAsync_NotExistsLyrics_ShouldReturnNoDataFoundStatus(string artist, string song) { // Arrange @@ -132,7 +132,7 @@ public async Task SearchLyricAsync_NotExistsLyrics_ShouldReturnNoDataFoundStatus var searchRequest = new ArtistAndSongSearchRequest(artist, song); // Act - var searchResult = await lyricsClient.SearchLyricAsync(searchRequest); + var searchResult = await lyricsClient.SearchLyricAsync(searchRequest, TestContext.Current.CancellationToken); // Assert Assert.NotNull(searchResult); diff --git a/Tests/LyricsScraperNET.IntegrationTest/Providers/KPopLyrics/KPopLyricsProviderTest.cs b/Tests/LyricsScraperNET.IntegrationTest/Providers/KPopLyrics/KPopLyricsProviderTest.cs index f199696..fbb50e5 100644 --- a/Tests/LyricsScraperNET.IntegrationTest/Providers/KPopLyrics/KPopLyricsProviderTest.cs +++ b/Tests/LyricsScraperNET.IntegrationTest/Providers/KPopLyrics/KPopLyricsProviderTest.cs @@ -7,23 +7,15 @@ using LyricsScraperNET.TestShared.Providers; using LyricsScraperNET.TestShared.TestModel; using Xunit; -using Xunit.Abstractions; namespace LyricsScraperNET.IntegrationTest.Providers.KPopLyrics { - public class KPopLyricsProviderTest : ProviderTestBase + public class KPopLyricsProviderTest(ITestOutputHelper testOutputHelper) : ProviderTestBase { - private readonly ITestOutputHelper _testOutputHelper; - - public KPopLyricsProviderTest(ITestOutputHelper testOutputHelper) - { - _testOutputHelper = testOutputHelper; - } - #region sync [Theory] - [MemberData(nameof(GetTestData), parameters: "Providers\\KPopLyrics\\lyric_test_data.json")] + [MemberData(nameof(GetTestData), arguments: "Providers\\KPopLyrics\\lyric_test_data.json")] public void SearchLyric_IntegrationDynamicData_Success(LyricsTestData testData) { // Arrange @@ -41,8 +33,8 @@ public void SearchLyric_IntegrationDynamicData_Success(LyricsTestData testData) Assert.True(string.IsNullOrEmpty(searchResult.ResponseMessage)); Assert.Equal(ExternalProviderType.KPopLyrics, searchResult.ExternalProviderType); Assert.Equal(testData.LyricResultData.Replace("\r\n", "\n"), searchResult.LyricText.Replace("\r\n", "\n")); - _testOutputHelper.WriteLine(testData.LyricResultData.Length.ToString()); - _testOutputHelper.WriteLine(searchResult.LyricText.Length.ToString()); + testOutputHelper.WriteLine(testData.LyricResultData.Length.ToString()); + testOutputHelper.WriteLine(searchResult.LyricText.Length.ToString()); } [Theory] @@ -71,7 +63,7 @@ public void SearchLyric_NotExistsLyrics_ShouldReturnNoDataFoundStatus(string art #region async [Theory] - [MemberData(nameof(GetTestData), parameters: "Providers\\KPopLyrics\\lyric_test_data.json")] + [MemberData(nameof(GetTestData), arguments: "Providers\\KPopLyrics\\lyric_test_data.json")] public async Task SearchLyricAsync_IntegrationDynamicData_Success(LyricsTestData testData) { // Arrange diff --git a/Tests/LyricsScraperNET.IntegrationTest/Providers/LyricFind/LyricFindProviderTest.cs b/Tests/LyricsScraperNET.IntegrationTest/Providers/LyricFind/LyricFindProviderTest.cs index b63ca65..07776d3 100644 --- a/Tests/LyricsScraperNET.IntegrationTest/Providers/LyricFind/LyricFindProviderTest.cs +++ b/Tests/LyricsScraperNET.IntegrationTest/Providers/LyricFind/LyricFindProviderTest.cs @@ -16,7 +16,7 @@ public class LyricFindProviderTest : ProviderTestBase #region sync [Theory] - [MemberData(nameof(GetTestData), parameters: "Providers\\LyricFind\\lyric_test_data.json")] + [MemberData(nameof(GetTestData), arguments: "Providers\\LyricFind\\lyric_test_data.json")] public void SearchLyric_IntegrationDynamicData_Success(LyricsTestData testData) { // Arrange @@ -37,7 +37,7 @@ public void SearchLyric_IntegrationDynamicData_Success(LyricsTestData testData) Assert.False(searchResult.Instrumental); } - [RegionalTestTheory(excludeRegions: new[] { "AM", "RU" })] + [RegionalTestTheory(excludeRegions: ["AM", "RU"])] [InlineData("rush", "yyz")] public void SearchLyric_Instrumental_ShouldReturnSuccess(string artist, string song) { @@ -58,7 +58,7 @@ public void SearchLyric_Instrumental_ShouldReturnSuccess(string artist, string s Assert.True(searchResult.Instrumental); } - [RegionalTestTheory(includeRegions: new[] { "AM", "RU" })] + [RegionalTestTheory(includeRegions: ["AM", "RU"])] [InlineData("rush", "Tom Sawyer")] public void SearchLyric_LyricAreNotAvailableInRegion_ShouldRegionRestrictedStatus(string artist, string song) { @@ -104,7 +104,7 @@ public void SearchLyric_NotExistsLyrics_ShouldReturnNoDataFoundStatus(string art #region async [Theory] - [MemberData(nameof(GetTestData), parameters: "Providers\\LyricFind\\lyric_test_data.json")] + [MemberData(nameof(GetTestData), arguments: "Providers\\LyricFind\\lyric_test_data.json")] public async Task SearchLyricAsync_IntegrationDynamicData_Success(LyricsTestData testData) { // Arrange @@ -112,7 +112,7 @@ public async Task SearchLyricAsync_IntegrationDynamicData_Success(LyricsTestData SearchRequest searchRequest = CreateSearchRequest(testData); // Act - var searchResult = await lyricsClient.SearchLyricAsync(searchRequest); + var searchResult = await lyricsClient.SearchLyricAsync(searchRequest, TestContext.Current.CancellationToken); // Assert Assert.NotNull(searchResult); @@ -124,7 +124,7 @@ public async Task SearchLyricAsync_IntegrationDynamicData_Success(LyricsTestData Assert.False(searchResult.Instrumental); } - [RegionalTestTheory(excludeRegions: new[] { "AM", "RU" })] + [RegionalTestTheory(excludeRegions: ["AM", "RU"])] [InlineData("rush", "yyz")] public async Task SearchLyricAsync_Instrumental_ShouldReturnSuccess(string artist, string song) { @@ -144,7 +144,7 @@ public async Task SearchLyricAsync_Instrumental_ShouldReturnSuccess(string artis Assert.True(searchResult.Instrumental); } - [RegionalTestTheory(includeRegions: new[] { "AM", "RU" })] + [RegionalTestTheory(includeRegions: ["AM", "RU"])] [InlineData("rush", "Tom Sawyer")] public async Task SearchLyricAsync_LyricAreNotAvailableInRegion_ShouldRegionRestrictedStatus(string artist, string song) { @@ -173,7 +173,7 @@ public async Task SearchLyricAsync_NotExistsLyrics_ShouldReturnNoDataFoundStatus var searchRequest = new ArtistAndSongSearchRequest(artist, song); // Act - var searchResult = await lyricsClient.SearchLyricAsync(searchRequest); + var searchResult = await lyricsClient.SearchLyricAsync(searchRequest, TestContext.Current.CancellationToken); // Assert Assert.NotNull(searchResult); diff --git a/Tests/LyricsScraperNET.IntegrationTest/Providers/LyricsFreak/LyricsFreakProviderTest.cs b/Tests/LyricsScraperNET.IntegrationTest/Providers/LyricsFreak/LyricsFreakProviderTest.cs index ed1d35f..3839bcf 100644 --- a/Tests/LyricsScraperNET.IntegrationTest/Providers/LyricsFreak/LyricsFreakProviderTest.cs +++ b/Tests/LyricsScraperNET.IntegrationTest/Providers/LyricsFreak/LyricsFreakProviderTest.cs @@ -16,7 +16,7 @@ public class LyricsFreakProviderTest : ProviderTestBase #region Sync [Theory] - [MemberData(nameof(GetTestData), parameters: "Providers\\LyricsFreak\\lyric_test_data.json")] + [MemberData(nameof(GetTestData), arguments: "Providers\\LyricsFreak\\lyric_test_data.json")] public void SearchLyric_IntegrationDynamicData_Success(LyricsTestData testData) { // Arrange @@ -63,7 +63,7 @@ public void SearchLyric_NotExistsLyrics_ShouldReturnNoDataFoundStatus(string art #region Async [Theory] - [MemberData(nameof(GetTestData), parameters: "Providers\\LyricsFreak\\lyric_test_data.json")] + [MemberData(nameof(GetTestData), arguments: "Providers\\LyricsFreak\\lyric_test_data.json")] public async Task SearchLyricAsync_IntegrationDynamicData_Success(LyricsTestData testData) { // Arrange diff --git a/Tests/LyricsScraperNET.IntegrationTest/Providers/Musixmatch/MusixmatchProviderTest.cs b/Tests/LyricsScraperNET.IntegrationTest/Providers/Musixmatch/MusixmatchProviderTest.cs index fc2484d..fa627be 100644 --- a/Tests/LyricsScraperNET.IntegrationTest/Providers/Musixmatch/MusixmatchProviderTest.cs +++ b/Tests/LyricsScraperNET.IntegrationTest/Providers/Musixmatch/MusixmatchProviderTest.cs @@ -15,7 +15,7 @@ public class MusixmatchProviderTest : ProviderTestBase #region sync [Theory] - [MemberData(nameof(GetTestData), parameters: "Providers\\Musixmatch\\lyric_test_data.json")] + [MemberData(nameof(GetTestData), arguments: "Providers\\Musixmatch\\lyric_test_data.json")] public void SearchLyric_IntegrationDynamicData_Success(LyricsTestData testData) { // Arrange @@ -37,7 +37,7 @@ public void SearchLyric_IntegrationDynamicData_Success(LyricsTestData testData) } [Theory] - [MemberData(nameof(GetTestData), parameters: "Providers\\Musixmatch\\instrumental_test_data.json")] + [MemberData(nameof(GetTestData), arguments: "Providers\\Musixmatch\\instrumental_test_data.json")] public void SearchLyric_IntegrationDynamicData_Instrumental(LyricsTestData testData) { // Arrange @@ -83,7 +83,7 @@ public void SearchLyric_NotExistsLyrics_ShouldReturnNoDataFoundStatus(string art #region async [Theory] - [MemberData(nameof(GetTestData), parameters: "Providers\\Musixmatch\\lyric_test_data.json")] + [MemberData(nameof(GetTestData), arguments: "Providers\\Musixmatch\\lyric_test_data.json")] public async Task SearchLyricAsync_IntegrationDynamicData_Success(LyricsTestData testData) { // Arrange @@ -91,7 +91,7 @@ public async Task SearchLyricAsync_IntegrationDynamicData_Success(LyricsTestData SearchRequest searchRequest = CreateSearchRequest(testData); // Act - var searchResult = await lyricsClient.SearchLyricAsync(searchRequest); + var searchResult = await lyricsClient.SearchLyricAsync(searchRequest, TestContext.Current.CancellationToken); // Assert Assert.NotNull(searchResult); @@ -104,7 +104,7 @@ public async Task SearchLyricAsync_IntegrationDynamicData_Success(LyricsTestData } [Theory] - [MemberData(nameof(GetTestData), parameters: "Providers\\Musixmatch\\instrumental_test_data.json")] + [MemberData(nameof(GetTestData), arguments: "Providers\\Musixmatch\\instrumental_test_data.json")] public async Task SearchLyricAsync_IntegrationDynamicData_Instrumental(LyricsTestData testData) { // Arrange @@ -112,7 +112,7 @@ public async Task SearchLyricAsync_IntegrationDynamicData_Instrumental(LyricsTes SearchRequest searchRequest = CreateSearchRequest(testData); // Act - var searchResult = await lyricsClient.SearchLyricAsync(searchRequest); + var searchResult = await lyricsClient.SearchLyricAsync(searchRequest, TestContext.Current.CancellationToken); // Assert Assert.NotNull(searchResult); @@ -132,7 +132,7 @@ public async Task SearchLyricAsync_NotExistsLyrics_ShouldReturnNoDataFoundStatus var searchRequest = new ArtistAndSongSearchRequest(artist, song); // Act - var searchResult = await lyricsClient.SearchLyricAsync(searchRequest); + var searchResult = await lyricsClient.SearchLyricAsync(searchRequest, TestContext.Current.CancellationToken); // Assert Assert.NotNull(searchResult); diff --git a/Tests/LyricsScraperNET.IntegrationTest/Providers/SongLyrics/SongLyricsProviderTest.cs b/Tests/LyricsScraperNET.IntegrationTest/Providers/SongLyrics/SongLyricsProviderTest.cs index 01a94a5..7546db0 100644 --- a/Tests/LyricsScraperNET.IntegrationTest/Providers/SongLyrics/SongLyricsProviderTest.cs +++ b/Tests/LyricsScraperNET.IntegrationTest/Providers/SongLyrics/SongLyricsProviderTest.cs @@ -15,7 +15,7 @@ public class SongLyricsProviderTest : ProviderTestBase #region sync [Theory] - [MemberData(nameof(GetTestData), parameters: "Providers\\SongLyrics\\lyric_test_data.json")] + [MemberData(nameof(GetTestData), arguments: "Providers\\SongLyrics\\lyric_test_data.json")] public void SearchLyric_IntegrationDynamicData_Success(LyricsTestData testData) { // Arrange @@ -83,7 +83,7 @@ public void SearchLyric_NotExistsLyrics_ShouldReturnNoDataFoundStatus(string art #region async [Theory] - [MemberData(nameof(GetTestData), parameters: "Providers\\SongLyrics\\lyric_test_data.json")] + [MemberData(nameof(GetTestData), arguments: "Providers\\SongLyrics\\lyric_test_data.json")] public async Task SearchLyricAsync_IntegrationDynamicData_Success(LyricsTestData testData) { // Arrange @@ -91,7 +91,7 @@ public async Task SearchLyricAsync_IntegrationDynamicData_Success(LyricsTestData SearchRequest searchRequest = CreateSearchRequest(testData); // Act - var searchResult = await lyricsClient.SearchLyricAsync(searchRequest); + var searchResult = await lyricsClient.SearchLyricAsync(searchRequest, TestContext.Current.CancellationToken); // Assert Assert.NotNull(searchResult); @@ -112,7 +112,7 @@ public async Task SearchLyricAsync_Instrumental_ShouldReturnSuccess(string artis var searchRequest = new ArtistAndSongSearchRequest(artist, song); // Act - var searchResult = await lyricsClient.SearchLyricAsync(searchRequest); + var searchResult = await lyricsClient.SearchLyricAsync(searchRequest, TestContext.Current.CancellationToken); // Assert Assert.NotNull(searchResult); @@ -132,7 +132,7 @@ public async Task SearchLyricAsync_NotExistsLyrics_ShouldReturnNoDataFoundStatus var searchRequest = new ArtistAndSongSearchRequest(artist, song); // Act - var searchResult = await lyricsClient.SearchLyricAsync(searchRequest); + var searchResult = await lyricsClient.SearchLyricAsync(searchRequest, TestContext.Current.CancellationToken); // Assert Assert.NotNull(searchResult); diff --git a/Tests/LyricsScraperNET.TestShared/LyricsScraperNET.TestShared.csproj b/Tests/LyricsScraperNET.TestShared/LyricsScraperNET.TestShared.csproj index f1b72cd..aa5088c 100644 --- a/Tests/LyricsScraperNET.TestShared/LyricsScraperNET.TestShared.csproj +++ b/Tests/LyricsScraperNET.TestShared/LyricsScraperNET.TestShared.csproj @@ -5,13 +5,12 @@ - net8.0;net7.0;net6.0;net5.0;netstandard2.1;netstandard2.0 + net10.0;net9.0;net8.0 - - - + + diff --git a/Tests/LyricsScraperNET.UnitTest/LyricsScraperClientTests.cs b/Tests/LyricsScraperNET.UnitTest/LyricsScraperClientTests.cs index a606de4..459ccc7 100644 --- a/Tests/LyricsScraperNET.UnitTest/LyricsScraperClientTests.cs +++ b/Tests/LyricsScraperNET.UnitTest/LyricsScraperClientTests.cs @@ -386,7 +386,7 @@ public async Task SearchLyricAsync_Should_Not_Cancel_If_Token_Default() var client = GetLyricsScraperClientWithMockedProvider(); // Act - var result = await client.SearchLyricAsync(searchRequest); + var result = await client.SearchLyricAsync(searchRequest, TestContext.Current.CancellationToken); // Assert Assert.NotNull(result); @@ -422,8 +422,10 @@ public void UseParallelSearchEnabled_ShouldUseLocalVariable_WhenExplicitlySet( // Arrange var configuration = new LyricScraperClientConfig { UseParallelSearch = configValue }; var mockedProviders = new[] { GetExternalProviderMock(ExternalProviderType.None) }; - var client = new LyricsScraperClient(configuration, mockedProviders); - client.UseParallelSearch = variableValue; + var client = new LyricsScraperClient(configuration, mockedProviders) + { + UseParallelSearch = variableValue + }; // Act var actualResult = client.UseParallelSearch; @@ -476,7 +478,7 @@ public async Task SearchLyricAsync_WithUseParallelSearchEnabled_ShouldReturnFirs var client = new LyricsScraperClient(config, providers); // Act - var result = await client.SearchLyricAsync(searchRequest); + var result = await client.SearchLyricAsync(searchRequest, TestContext.Current.CancellationToken); // Assert // Verify the result matches the fast provider's response. @@ -537,7 +539,7 @@ public async Task SearchLyricAsync_WithUseParallelSearchEnabled_ShouldReturnResu var client = new LyricsScraperClient(config, providers); // Act - var result = await client.SearchLyricAsync(searchRequest); + var result = await client.SearchLyricAsync(searchRequest, TestContext.Current.CancellationToken); // Assert Assert.Equal("Slow result", result.LyricText); @@ -638,7 +640,7 @@ public async Task SearchLyricAsync_WithUseParallelSearchEnabled_ShouldReturnResu var client = new LyricsScraperClient(config, providers); // Act - var result = await client.SearchLyricAsync(searchRequest); + var result = await client.SearchLyricAsync(searchRequest, TestContext.Current.CancellationToken); // Assert // Verify that the result comes from the slow provider. @@ -689,7 +691,7 @@ public async Task SearchLyricAsync_WithUseParallelSearchDisabled_ShouldReturnRes var client = new LyricsScraperClient(config, providers); // Act - var result = await client.SearchLyricAsync(searchRequest); + var result = await client.SearchLyricAsync(searchRequest, TestContext.Current.CancellationToken); // Assert // Verify that the result comes from the slow provider. @@ -706,19 +708,19 @@ public async Task SearchLyricAsync_WithUseParallelSearchDisabled_ShouldReturnRes #region helpers - private ExternalProviderType[] GetExternalProviderTypes() + private static ExternalProviderType[] GetExternalProviderTypes() { - return new[] { ExternalProviderType.AZLyrics, ExternalProviderType.SongLyrics }; + return [ExternalProviderType.AZLyrics, ExternalProviderType.SongLyrics]; } - private ILyricsScraperClient GetLyricsScraperClient() + private static ILyricsScraperClient GetLyricsScraperClient() { return new LyricsScraperClient() .WithAZLyrics() .WithSongLyrics(); } - private ILyricsScraperClient GetLyricsScraperClientWithMockedProvider() + private static LyricsScraperClient GetLyricsScraperClientWithMockedProvider() { var client = new LyricsScraperClient(); var externalProvider = GetExternalProviderMock(ExternalProviderType.AZLyrics); @@ -727,7 +729,7 @@ private ILyricsScraperClient GetLyricsScraperClientWithMockedProvider() return client; } - private IExternalProvider GetExternalProviderMock(ExternalProviderType externalProviderType) + private static IExternalProvider GetExternalProviderMock(ExternalProviderType externalProviderType) { var externalProviderMock = A.Fake(); @@ -750,7 +752,7 @@ private IExternalProvider GetExternalProviderMock(ExternalProviderType externalP return externalProviderMock; } - private SearchRequest GetSearchRequestMock() + private static SearchRequest GetSearchRequestMock() { var searchRequestMock = A.Fake(); string error = string.Empty; diff --git a/Tests/LyricsScraperNET.UnitTest/LyricsScraperNET.UnitTest.csproj b/Tests/LyricsScraperNET.UnitTest/LyricsScraperNET.UnitTest.csproj index 993129a..6b6cb46 100644 --- a/Tests/LyricsScraperNET.UnitTest/LyricsScraperNET.UnitTest.csproj +++ b/Tests/LyricsScraperNET.UnitTest/LyricsScraperNET.UnitTest.csproj @@ -5,36 +5,26 @@ - net8.0;net7.0;net6.0;net5.0;netcoreapp3.1 - 9.0 + net10.0;net9.0;net8.0 + latest false - - - - - + + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/Tests/LyricsScraperNET.UnitTest/Providers/AZLyrics/AZLyricsProviderTest.cs b/Tests/LyricsScraperNET.UnitTest/Providers/AZLyrics/AZLyricsProviderTest.cs index 72b46d6..a92fadd 100644 --- a/Tests/LyricsScraperNET.UnitTest/Providers/AZLyrics/AZLyricsProviderTest.cs +++ b/Tests/LyricsScraperNET.UnitTest/Providers/AZLyrics/AZLyricsProviderTest.cs @@ -13,7 +13,7 @@ namespace LyricsScraperNET.UnitTest.Providers.AZLyrics public class AZLyricsProviderTest : ProviderTestBase { [Theory] - [MemberData(nameof(GetTestData), parameters: "Providers\\AZLyrics\\lyric_test_data.json")] + [MemberData(nameof(GetTestData), arguments: "Providers\\AZLyrics\\lyric_test_data.json")] public void SearchLyric_UnitDynamicData_Success(LyricsTestData testData) { // Arrange diff --git a/Tests/LyricsScraperNET.UnitTest/Providers/Genius/GeniusProviderTest.cs b/Tests/LyricsScraperNET.UnitTest/Providers/Genius/GeniusProviderTest.cs index cad143e..15b7b49 100644 --- a/Tests/LyricsScraperNET.UnitTest/Providers/Genius/GeniusProviderTest.cs +++ b/Tests/LyricsScraperNET.UnitTest/Providers/Genius/GeniusProviderTest.cs @@ -13,7 +13,7 @@ namespace LyricsScraperNET.UnitTest.Providers.Genius public class GeniusProviderTest : ProviderTestBase { [Theory] - [MemberData(nameof(GetTestData), parameters: "Providers\\Genius\\lyric_test_data.json")] + [MemberData(nameof(GetTestData), arguments: "Providers\\Genius\\lyric_test_data.json")] public void SearchLyric_UnitDynamicData_Success(LyricsTestData testData) { // Arrange @@ -37,7 +37,7 @@ public void SearchLyric_UnitDynamicData_Success(LyricsTestData testData) } [Theory] - [MemberData(nameof(GetTestData), parameters: "Providers\\Genius\\instrumental_test_data.json")] + [MemberData(nameof(GetTestData), arguments: "Providers\\Genius\\instrumental_test_data.json")] public void SearchLyric_UnitDynamicData_Instrumental(LyricsTestData testData) { // Arrange diff --git a/Tests/LyricsScraperNET.UnitTest/Providers/KPopLyrics/KPopLyricsProviderTest.cs b/Tests/LyricsScraperNET.UnitTest/Providers/KPopLyrics/KPopLyricsProviderTest.cs index 31ad3ef..e04edfe 100644 --- a/Tests/LyricsScraperNET.UnitTest/Providers/KPopLyrics/KPopLyricsProviderTest.cs +++ b/Tests/LyricsScraperNET.UnitTest/Providers/KPopLyrics/KPopLyricsProviderTest.cs @@ -5,14 +5,14 @@ using LyricsScraperNET.TestShared.Extensions; using LyricsScraperNET.TestShared.Providers; using LyricsScraperNET.TestShared.TestModel; -using Microsoft.VisualStudio.TestTools.UnitTesting; +using Xunit; namespace LyricsScraperNET.UnitTest.Providers.KPopLyrics { public class KPopLyricsProviderTest : ProviderTestBase { [Theory] - [MemberData(nameof(GetTestData), parameters: "Providers\\KPopLyrics\\lyric_test_data.json")] + [MemberData(nameof(GetTestData), arguments: "Providers\\KPopLyrics\\lyric_test_data.json")] public void SearchLyric_UnitDynamicData_Success(LyricsTestData testData) { // Arrange diff --git a/Tests/LyricsScraperNET.UnitTest/Providers/LyricFind/LyricFindProviderTest.cs b/Tests/LyricsScraperNET.UnitTest/Providers/LyricFind/LyricFindProviderTest.cs index 22a7696..9917bcd 100644 --- a/Tests/LyricsScraperNET.UnitTest/Providers/LyricFind/LyricFindProviderTest.cs +++ b/Tests/LyricsScraperNET.UnitTest/Providers/LyricFind/LyricFindProviderTest.cs @@ -13,7 +13,7 @@ namespace LyricsScraperNET.UnitTest.Providers.LyricFind public class LyricFindProviderTest : ProviderTestBase { [Theory] - [MemberData(nameof(GetTestData), parameters: "Providers\\LyricFind\\lyric_test_data.json")] + [MemberData(nameof(GetTestData), arguments: "Providers\\LyricFind\\lyric_test_data.json")] public void SearchLyric_UnitDynamicData_Success(LyricsTestData testData) { // Arrange @@ -36,7 +36,7 @@ public void SearchLyric_UnitDynamicData_Success(LyricsTestData testData) } [Theory] - [MemberData(nameof(GetTestData), parameters: "Providers\\LyricFind\\instrumental_test_data.json")] + [MemberData(nameof(GetTestData), arguments: "Providers\\LyricFind\\instrumental_test_data.json")] public void SearchLyric_UnitDynamicData_Instrumental(LyricsTestData testData) { // Arrange @@ -59,7 +59,7 @@ public void SearchLyric_UnitDynamicData_Instrumental(LyricsTestData testData) } [Theory] - [MemberData(nameof(GetTestData), parameters: "Providers\\LyricFind\\region_restricted_test_data.json")] + [MemberData(nameof(GetTestData), arguments: "Providers\\LyricFind\\region_restricted_test_data.json")] public void SearchLyric_UnitDynamicData_RegionRestricted(LyricsTestData testData) { // Arrange @@ -69,7 +69,7 @@ public void SearchLyric_UnitDynamicData_RegionRestricted(LyricsTestData testData SearchRequest searchRequest = CreateSearchRequest(testData); // Act - var searchResult = lyricsClient.SearchLyric(searchRequest); + var searchResult = lyricsClient.SearchLyric(searchRequest, TestContext.Current.CancellationToken); // Assert Assert.NotNull(searchResult); @@ -81,7 +81,7 @@ public void SearchLyric_UnitDynamicData_RegionRestricted(LyricsTestData testData } [Theory] - [MemberData(nameof(GetTestData), parameters: "Providers\\LyricFind\\not_found_test_data.json")] + [MemberData(nameof(GetTestData), arguments: "Providers\\LyricFind\\not_found_test_data.json")] public void SearchLyric_UnitDynamicData_NotFound(LyricsTestData testData) { // Arrange @@ -91,7 +91,7 @@ public void SearchLyric_UnitDynamicData_NotFound(LyricsTestData testData) SearchRequest searchRequest = CreateSearchRequest(testData); // Act - var searchResult = lyricsClient.SearchLyric(searchRequest); + var searchResult = lyricsClient.SearchLyric(searchRequest, TestContext.Current.CancellationToken); // Assert Assert.NotNull(searchResult); diff --git a/Tests/LyricsScraperNET.UnitTest/Providers/LyricsFreak/LyricsFreakProviderTest.cs b/Tests/LyricsScraperNET.UnitTest/Providers/LyricsFreak/LyricsFreakProviderTest.cs index a8cd8c8..b78e704 100644 --- a/Tests/LyricsScraperNET.UnitTest/Providers/LyricsFreak/LyricsFreakProviderTest.cs +++ b/Tests/LyricsScraperNET.UnitTest/Providers/LyricsFreak/LyricsFreakProviderTest.cs @@ -13,7 +13,7 @@ namespace LyricsScraperNET.UnitTest.Providers.LyricsFreak public class LyricsFreakProviderTest : ProviderTestBase { [Theory] - [MemberData(nameof(GetTestData), parameters: "Providers\\LyricsFreak\\lyric_test_data.json")] + [MemberData(nameof(GetTestData), arguments: "Providers\\LyricsFreak\\lyric_test_data.json")] public void SearchLyric_UnitDynamicData_Success(LyricsTestData testData) { // Arrange diff --git a/Tests/LyricsScraperNET.UnitTest/Providers/SongLyrics/SongLyricsProviderTest.cs b/Tests/LyricsScraperNET.UnitTest/Providers/SongLyrics/SongLyricsProviderTest.cs index abeaf4b..f18716b 100644 --- a/Tests/LyricsScraperNET.UnitTest/Providers/SongLyrics/SongLyricsProviderTest.cs +++ b/Tests/LyricsScraperNET.UnitTest/Providers/SongLyrics/SongLyricsProviderTest.cs @@ -13,7 +13,7 @@ namespace LyricsScraperNET.UnitTest.Providers.SongLyrics public class SongLyricsProviderTest : ProviderTestBase { [Theory] - [MemberData(nameof(GetTestData), parameters: "Providers\\SongLyrics\\lyric_test_data.json")] + [MemberData(nameof(GetTestData), arguments: "Providers\\SongLyrics\\lyric_test_data.json")] public void SearchLyric_UnitDynamicData_Success(LyricsTestData testData) { // Arrange @@ -36,7 +36,7 @@ public void SearchLyric_UnitDynamicData_Success(LyricsTestData testData) } [Theory] - [MemberData(nameof(GetTestData), parameters: "Providers\\SongLyrics\\instrumental_test_data.json")] + [MemberData(nameof(GetTestData), arguments: "Providers\\SongLyrics\\instrumental_test_data.json")] public void SearchLyric_UnitDynamicData_Instrumental(LyricsTestData testData) { // Arrange From be9d9852b719232a366673ab8426d7837333e230 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=92=D0=B8=D1=82=D0=B0=D0=BB=D0=B8=D0=B9=20=D0=9A=D0=B2?= =?UTF-8?q?=D0=B0=D1=88=D0=B8=D0=BD?= Date: Sun, 30 Aug 2026 20:31:41 +0200 Subject: [PATCH 02/11] #52 Added Lrclib provider and fixed flaky test --- LyricsScraperNET.Client/appsettings.json | 4 + LyricsScraperNET/Common/Constants.cs | 5 +- .../ILyricScraperClientConfig.cs | 2 + .../Configuration/LyricScraperClientConfig.cs | 6 +- .../ServiceCollectionExtensions.cs | 2 + .../LyricsScraperClientExtensions.cs | 10 +- .../Network/HtmlAgilityWebClient.cs | 6 +- .../Providers/AZLyrics/AZLyricsOptions.cs | 7 +- .../Providers/KPopLyrics/KPopLyricsOptions.cs | 7 +- .../Providers/Lrclib/LrclibHttpClient.cs | 113 +++++++++++++ .../Providers/Lrclib/LrclibOptions.cs | 28 ++++ .../Providers/Lrclib/LrclibParser.cs | 12 ++ .../Providers/Lrclib/LrclibProvider.cs | 155 ++++++++++++++++++ .../Providers/Lrclib/LrclibTrackResponse.cs | 19 +++ .../Providers/Lrclib/LrclibUriConverter.cs | 21 +++ .../Providers/LyricFind/LyricFindProvider.cs | 12 +- .../LyricsFreak/LyricsFreakOptions.cs | 7 +- .../LyricsFreak/LyricsFreakProvider.cs | 6 +- .../Providers/Models/ExternalProviderType.cs | 3 +- .../Providers/SongLyrics/SongLyricsOptions.cs | 7 +- README.md | 1 + .../LyricsScraperNET.IntegrationTest.csproj | 6 + .../Providers/Lrclib/LrclibProviderTest.cs | 107 ++++++++++++ .../Lrclib/Resources/Lyrics_Result_01.txt | 36 ++++ .../Providers/Lrclib/lyric_test_data.json | 14 ++ .../ServiceCollectionExtensionsTest.cs | 3 +- .../LyricsScraperClientExtensionsTest.cs | 15 ++ .../LyricsScraperClientTests.cs | 6 +- .../LyricsScraperNET.UnitTest.csproj | 21 +++ .../Providers/Lrclib/LrclibProviderTest.cs | 83 ++++++++++ .../Lrclib/LrclibUriConverterTests.cs | 25 +++ .../Lrclib/Resources/Instrumental_Json_01.txt | 1 + .../Lrclib/Resources/Lyrics_Json_01.txt | 1 + .../Lrclib/Resources/Lyrics_Result_01.txt | 36 ++++ .../Lrclib/Resources/NotFound_Json_01.txt | 1 + .../Lrclib/instrumental_test_data.json | 9 + .../Providers/Lrclib/lyric_test_data.json | 9 + .../Providers/Lrclib/not_found_test_data.json | 9 + .../Resources/full_test_settings.json | 4 + 39 files changed, 778 insertions(+), 41 deletions(-) create mode 100644 LyricsScraperNET/Providers/Lrclib/LrclibHttpClient.cs create mode 100644 LyricsScraperNET/Providers/Lrclib/LrclibOptions.cs create mode 100644 LyricsScraperNET/Providers/Lrclib/LrclibParser.cs create mode 100644 LyricsScraperNET/Providers/Lrclib/LrclibProvider.cs create mode 100644 LyricsScraperNET/Providers/Lrclib/LrclibTrackResponse.cs create mode 100644 LyricsScraperNET/Providers/Lrclib/LrclibUriConverter.cs create mode 100644 Tests/LyricsScraperNET.IntegrationTest/Providers/Lrclib/LrclibProviderTest.cs create mode 100644 Tests/LyricsScraperNET.IntegrationTest/Providers/Lrclib/Resources/Lyrics_Result_01.txt create mode 100644 Tests/LyricsScraperNET.IntegrationTest/Providers/Lrclib/lyric_test_data.json create mode 100644 Tests/LyricsScraperNET.UnitTest/Providers/Lrclib/LrclibProviderTest.cs create mode 100644 Tests/LyricsScraperNET.UnitTest/Providers/Lrclib/LrclibUriConverterTests.cs create mode 100644 Tests/LyricsScraperNET.UnitTest/Providers/Lrclib/Resources/Instrumental_Json_01.txt create mode 100644 Tests/LyricsScraperNET.UnitTest/Providers/Lrclib/Resources/Lyrics_Json_01.txt create mode 100644 Tests/LyricsScraperNET.UnitTest/Providers/Lrclib/Resources/Lyrics_Result_01.txt create mode 100644 Tests/LyricsScraperNET.UnitTest/Providers/Lrclib/Resources/NotFound_Json_01.txt create mode 100644 Tests/LyricsScraperNET.UnitTest/Providers/Lrclib/instrumental_test_data.json create mode 100644 Tests/LyricsScraperNET.UnitTest/Providers/Lrclib/lyric_test_data.json create mode 100644 Tests/LyricsScraperNET.UnitTest/Providers/Lrclib/not_found_test_data.json diff --git a/LyricsScraperNET.Client/appsettings.json b/LyricsScraperNET.Client/appsettings.json index 563b783..bfb30d5 100644 --- a/LyricsScraperNET.Client/appsettings.json +++ b/LyricsScraperNET.Client/appsettings.json @@ -30,6 +30,10 @@ "LyricsFreakOptions": { "SearchPriority": 3, "Enabled": true + }, + "LrclibOptions": { + "SearchPriority": 7, + "Enabled": true } } } \ No newline at end of file diff --git a/LyricsScraperNET/Common/Constants.cs b/LyricsScraperNET/Common/Constants.cs index a8e8f7f..bc179cf 100644 --- a/LyricsScraperNET/Common/Constants.cs +++ b/LyricsScraperNET/Common/Constants.cs @@ -13,9 +13,12 @@ internal static class Constants { ExternalProviderType.SongLyrics, 4}, { ExternalProviderType.LyricFind, 1}, { ExternalProviderType.KPopLyrics, 2}, - { ExternalProviderType.LyricsFreak, 3} + { ExternalProviderType.LyricsFreak, 3}, + { ExternalProviderType.Lrclib, 7} }; + internal const string LibraryUserAgent = "LyricsScraperNET/1.0 (https://github.com/skuill/LyricsScraperNET)"; + internal static class ResponseMessages { internal static readonly string ExternalProvidersListIsEmpty = "Empty external providers list! Please set any external provider first"; diff --git a/LyricsScraperNET/Configuration/ILyricScraperClientConfig.cs b/LyricsScraperNET/Configuration/ILyricScraperClientConfig.cs index 29855b0..5c27520 100644 --- a/LyricsScraperNET/Configuration/ILyricScraperClientConfig.cs +++ b/LyricsScraperNET/Configuration/ILyricScraperClientConfig.cs @@ -28,5 +28,7 @@ public interface ILyricScraperClientConfig IExternalProviderOptions KPopLyricsOptions { get; } IExternalProviderOptions LyricsFreakOptions { get; } + + IExternalProviderOptions LrclibOptions { get; } } } diff --git a/LyricsScraperNET/Configuration/LyricScraperClientConfig.cs b/LyricsScraperNET/Configuration/LyricScraperClientConfig.cs index 6b5f2c2..987973f 100644 --- a/LyricsScraperNET/Configuration/LyricScraperClientConfig.cs +++ b/LyricsScraperNET/Configuration/LyricScraperClientConfig.cs @@ -3,6 +3,7 @@ using LyricsScraperNET.Providers.Genius; using LyricsScraperNET.Providers.KPopLyrics; using LyricsScraperNET.Providers.LyricFind; +using LyricsScraperNET.Providers.Lrclib; using LyricsScraperNET.Providers.LyricsFreak; using LyricsScraperNET.Providers.Musixmatch; using LyricsScraperNET.Providers.SongLyrics; @@ -29,6 +30,8 @@ public sealed class LyricScraperClientConfig : ILyricScraperClientConfig public IExternalProviderOptions LyricsFreakOptions { get; set; } = new LyricsFreakOptions(); + public IExternalProviderOptions LrclibOptions { get; set; } = new LrclibOptions(); + /// public bool UseParallelSearch { get; set; } = false; @@ -39,6 +42,7 @@ public sealed class LyricScraperClientConfig : ILyricScraperClientConfig || SongLyricsOptions.Enabled || LyricFindOptions.Enabled || KPopLyricsOptions.Enabled - || LyricsFreakOptions.Enabled; + || LyricsFreakOptions.Enabled + || LrclibOptions.Enabled; } } diff --git a/LyricsScraperNET/Configuration/ServiceCollectionExtensions.cs b/LyricsScraperNET/Configuration/ServiceCollectionExtensions.cs index e359bc8..c4a36fb 100644 --- a/LyricsScraperNET/Configuration/ServiceCollectionExtensions.cs +++ b/LyricsScraperNET/Configuration/ServiceCollectionExtensions.cs @@ -3,6 +3,7 @@ using LyricsScraperNET.Providers.Genius; using LyricsScraperNET.Providers.KPopLyrics; using LyricsScraperNET.Providers.LyricFind; +using LyricsScraperNET.Providers.Lrclib; using LyricsScraperNET.Providers.LyricsFreak; using LyricsScraperNET.Providers.Musixmatch; using LyricsScraperNET.Providers.SongLyrics; @@ -34,6 +35,7 @@ public static IServiceCollection AddLyricScraperClientService( services.AddProvider(lyricScraperClientConfig); services.AddProvider(lyricScraperClientConfig); services.AddProvider(lyricScraperClientConfig); + services.AddProvider(lyricScraperClientConfig); services.AddMusixmatchService(lyricScraperClientConfig); diff --git a/LyricsScraperNET/Extensions/LyricsScraperClientExtensions.cs b/LyricsScraperNET/Extensions/LyricsScraperClientExtensions.cs index 3334b86..2419798 100644 --- a/LyricsScraperNET/Extensions/LyricsScraperClientExtensions.cs +++ b/LyricsScraperNET/Extensions/LyricsScraperClientExtensions.cs @@ -2,6 +2,7 @@ using LyricsScraperNET.Providers.Genius; using LyricsScraperNET.Providers.KPopLyrics; using LyricsScraperNET.Providers.LyricFind; +using LyricsScraperNET.Providers.Lrclib; using LyricsScraperNET.Providers.LyricsFreak; using LyricsScraperNET.Providers.Models; using LyricsScraperNET.Providers.Musixmatch; @@ -53,6 +54,12 @@ public static ILyricsScraperClient WithLyricsFreak(this ILyricsScraperClient lyr return lyricsScraperClient; } + public static ILyricsScraperClient WithLrclib(this ILyricsScraperClient lyricsScraperClient) + { + lyricsScraperClient.AddProvider(new LrclibProvider()); + return lyricsScraperClient; + } + ///

/// Configure LyricsScraperClient with all available providers in . /// Search lyrics enabled by default for all providers. @@ -66,7 +73,8 @@ public static ILyricsScraperClient WithAllProviders(this ILyricsScraperClient ly .WithSongLyrics() .WithLyricFind() .WithKPopLyrics() - .WithLyricsFreak(); + .WithLyricsFreak() + .WithLrclib(); } } } diff --git a/LyricsScraperNET/Network/HtmlAgilityWebClient.cs b/LyricsScraperNET/Network/HtmlAgilityWebClient.cs index c8828ea..a5470d4 100644 --- a/LyricsScraperNET/Network/HtmlAgilityWebClient.cs +++ b/LyricsScraperNET/Network/HtmlAgilityWebClient.cs @@ -19,8 +19,10 @@ internal sealed class HtmlAgilityWebClient : IWebClient public HtmlAgilityWebClient() { - _htmlWeb = new HtmlWeb(); - _htmlWeb.UsingCache = false; + _htmlWeb = new HtmlWeb + { + UsingCache = false + }; } public HtmlAgilityWebClient(ILogger logger) : this() diff --git a/LyricsScraperNET/Providers/AZLyrics/AZLyricsOptions.cs b/LyricsScraperNET/Providers/AZLyrics/AZLyricsOptions.cs index d38b3fc..67bbc86 100644 --- a/LyricsScraperNET/Providers/AZLyrics/AZLyricsOptions.cs +++ b/LyricsScraperNET/Providers/AZLyrics/AZLyricsOptions.cs @@ -22,12 +22,7 @@ public override bool Equals(object? obj) public override int GetHashCode() { - unchecked - { - int hash = 17; - hash = (hash * 31) + ExternalProviderType.GetHashCode(); - return hash; - } + return System.HashCode.Combine(ExternalProviderType); } } } diff --git a/LyricsScraperNET/Providers/KPopLyrics/KPopLyricsOptions.cs b/LyricsScraperNET/Providers/KPopLyrics/KPopLyricsOptions.cs index 5e6da84..cae52a6 100644 --- a/LyricsScraperNET/Providers/KPopLyrics/KPopLyricsOptions.cs +++ b/LyricsScraperNET/Providers/KPopLyrics/KPopLyricsOptions.cs @@ -22,12 +22,7 @@ public override bool Equals(object? obj) public override int GetHashCode() { - unchecked - { - int hash = 17; - hash = (hash * 31) + ExternalProviderType.GetHashCode(); - return hash; - } + return System.HashCode.Combine(ExternalProviderType); } } } \ No newline at end of file diff --git a/LyricsScraperNET/Providers/Lrclib/LrclibHttpClient.cs b/LyricsScraperNET/Providers/Lrclib/LrclibHttpClient.cs new file mode 100644 index 0000000..8f7a382 --- /dev/null +++ b/LyricsScraperNET/Providers/Lrclib/LrclibHttpClient.cs @@ -0,0 +1,113 @@ +using LyricsScraperNET.Common; +using LyricsScraperNET.Network.Abstract; +using Microsoft.Extensions.Logging; +using System; +using System.Net; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; + +namespace LyricsScraperNET.Providers.Lrclib +{ + /// + /// HTTP client for LRCLIB. Identifies the library via User-Agent and honors Retry-After on 429. + /// + internal sealed class LrclibHttpClient : IWebClient + { + private readonly ILogger? _logger; + private static readonly HttpClient _httpClient = new(); + private const int MaxAttempts = 2; + private static readonly TimeSpan MaxRetryAfter = TimeSpan.FromSeconds(30); + + public LrclibHttpClient() + { + } + + public LrclibHttpClient(ILogger logger) : this() + { + _logger = logger; + } + + public string Load(Uri uri, CancellationToken cancellationToken = default) + { + try + { + return LoadAsync(uri, cancellationToken).GetAwaiter().GetResult(); + } + catch (HttpRequestException ex) + { + _logger?.LogWarning($"Lrclib HTTP request failed for uri: {uri}. Exception: {ex}"); + return string.Empty; + } + } + + public async Task LoadAsync(Uri uri, CancellationToken cancellationToken = default) + { + try + { + for (int attempt = 1; attempt <= MaxAttempts; attempt++) + { + using var request = CreateRequest(uri); + var response = await _httpClient.SendAsync(request, cancellationToken); + + if (response.StatusCode == HttpStatusCode.TooManyRequests && attempt < MaxAttempts) + { + var delay = GetRetryAfterDelay(response); + _logger?.LogInformation($"Lrclib rate limited for uri: {uri}. Waiting {delay.TotalSeconds}s before retry."); + await Task.Delay(delay, cancellationToken); + continue; + } + + // 404 body is JSON with TrackNotFound and is handled by the provider. + if (response.StatusCode == HttpStatusCode.NotFound) + { + return await response.Content.ReadAsStringAsync(cancellationToken); + } + + response.EnsureSuccessStatusCode(); + var content = await response.Content.ReadAsStringAsync(cancellationToken); + if (string.IsNullOrWhiteSpace(content)) + { + _logger?.LogDebug($"Lrclib returned empty content for uri: {uri}"); + } + + return content; + } + } + catch (HttpRequestException ex) + { + _logger?.LogWarning($"Lrclib HTTP request failed for URI: {uri}. Exception: {ex}"); + return string.Empty; + } + catch (OperationCanceledException ex) + { + _logger?.LogInformation($"Lrclib request for URI: {uri} was canceled. Exception: {ex}"); + throw; + } + catch (Exception ex) + { + _logger?.LogError($"An unexpected error occurred while loading Lrclib URI: {uri}. Exception: {ex}"); + return string.Empty; + } + + return string.Empty; + } + + private static HttpRequestMessage CreateRequest(Uri uri) + { + var request = new HttpRequestMessage(HttpMethod.Get, uri); + request.Headers.TryAddWithoutValidation("User-Agent", Constants.LibraryUserAgent); + request.Headers.TryAddWithoutValidation("X-User-Agent", Constants.LibraryUserAgent); + return request; + } + + private static TimeSpan GetRetryAfterDelay(HttpResponseMessage response) + { + var retryAfter = response.Headers.RetryAfter?.Delta; + if (retryAfter == null || retryAfter.Value <= TimeSpan.Zero) + return TimeSpan.FromSeconds(1); + + return retryAfter.Value > MaxRetryAfter ? MaxRetryAfter : retryAfter.Value; + } + } +} diff --git a/LyricsScraperNET/Providers/Lrclib/LrclibOptions.cs b/LyricsScraperNET/Providers/Lrclib/LrclibOptions.cs new file mode 100644 index 0000000..f9b5ecb --- /dev/null +++ b/LyricsScraperNET/Providers/Lrclib/LrclibOptions.cs @@ -0,0 +1,28 @@ +using LyricsScraperNET.Common; +using LyricsScraperNET.Providers.Abstract; +using LyricsScraperNET.Providers.Models; + +namespace LyricsScraperNET.Providers.Lrclib +{ + public sealed class LrclibOptions : IExternalProviderOptions + { + public bool Enabled { get; set; } + + public ExternalProviderType ExternalProviderType => ExternalProviderType.Lrclib; + + public int SearchPriority { get; set; } = Constants.ProvidersSearchPriorities[ExternalProviderType.Lrclib]; + + public string ConfigurationSectionName { get; } = "LrclibOptions"; + + public override bool Equals(object? obj) + { + return obj is LrclibOptions options && + ExternalProviderType == options.ExternalProviderType; + } + + public override int GetHashCode() + { + return System.HashCode.Combine(ExternalProviderType); + } + } +} diff --git a/LyricsScraperNET/Providers/Lrclib/LrclibParser.cs b/LyricsScraperNET/Providers/Lrclib/LrclibParser.cs new file mode 100644 index 0000000..8265289 --- /dev/null +++ b/LyricsScraperNET/Providers/Lrclib/LrclibParser.cs @@ -0,0 +1,12 @@ +using LyricsScraperNET.Providers.Abstract; + +namespace LyricsScraperNET.Providers.Lrclib +{ + internal sealed class LrclibParser : IExternalProviderLyricParser + { + public string Parse(string lyric) + { + return lyric?.Trim() ?? string.Empty; + } + } +} diff --git a/LyricsScraperNET/Providers/Lrclib/LrclibProvider.cs b/LyricsScraperNET/Providers/Lrclib/LrclibProvider.cs new file mode 100644 index 0000000..7b9b2fb --- /dev/null +++ b/LyricsScraperNET/Providers/Lrclib/LrclibProvider.cs @@ -0,0 +1,155 @@ +using LyricsScraperNET.Extensions; +using LyricsScraperNET.Helpers; +using LyricsScraperNET.Models.Responses; +using LyricsScraperNET.Providers.Abstract; +using LyricsScraperNET.Providers.Models; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Options; +using System; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; + +namespace LyricsScraperNET.Providers.Lrclib +{ + public sealed class LrclibProvider : ExternalProviderBase + { + private ILogger? _logger; + private readonly IExternalUriConverter _uriConverter; + private static readonly JsonSerializerOptions JsonOptions = new() + { + PropertyNameCaseInsensitive = true + }; + + #region Constructors + + public LrclibProvider() + { + Parser = new LrclibParser(); + WebClient = new LrclibHttpClient(); + Options = new LrclibOptions() { Enabled = true }; + _uriConverter = new LrclibUriConverter(); + } + + public LrclibProvider(ILogger logger, LrclibOptions options) + : this() + { + _logger = logger; + Ensure.ArgumentNotNull(options, nameof(options)); + Options = options; + } + + public LrclibProvider(ILogger logger, IOptionsSnapshot options) + : this(logger, options.Value) + { + Ensure.ArgumentNotNull(options, nameof(options)); + } + + public LrclibProvider(LrclibOptions options) + : this(NullLogger.Instance, options) + { + Ensure.ArgumentNotNull(options, nameof(options)); + } + + public LrclibProvider(IOptionsSnapshot options) + : this(NullLogger.Instance, options.Value) + { + Ensure.ArgumentNotNull(options, nameof(options)); + } + + #endregion + + public override IExternalProviderOptions Options { get; } + + #region Sync + + protected override SearchResult SearchLyric(string artist, string song, CancellationToken cancellationToken = default) + { + return SearchLyricAsync(artist, song, cancellationToken).GetAwaiter().GetResult(); + } + + protected override SearchResult SearchLyric(Uri uri, CancellationToken cancellationToken = default) + { + return SearchLyricAsync(uri, cancellationToken).GetAwaiter().GetResult(); + } + + #endregion + + #region Async + + protected override async Task SearchLyricAsync(string artist, string song, CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + return await SearchLyricAsync(_uriConverter.GetLyricUri(artist, song), cancellationToken); + } + + protected override async Task SearchLyricAsync(Uri uri, CancellationToken cancellationToken = default) + { + if (WebClient == null || Parser == null) + { + _logger?.LogWarning($"Lrclib. Please set up WebClient and Parser first"); + return new SearchResult(ExternalProviderType.Lrclib); + } + + cancellationToken.ThrowIfCancellationRequested(); + + var text = await WebClient.LoadAsync(uri, cancellationToken); + + cancellationToken.ThrowIfCancellationRequested(); + + return PostProcessLyric(uri, text); + } + + #endregion + + public override void WithLogger(ILoggerFactory loggerFactory) + { + _logger = loggerFactory.CreateLogger(); + } + + private SearchResult PostProcessLyric(Uri uri, string text) + { + if (string.IsNullOrWhiteSpace(text)) + { + _logger?.LogWarning($"Lrclib. Response is empty for Uri: [{uri}]"); + return new SearchResult(ExternalProviderType.Lrclib); + } + + LrclibTrackResponse? response; + try + { + response = JsonSerializer.Deserialize(text, JsonOptions); + } + catch (JsonException ex) + { + _logger?.LogWarning($"Lrclib. Failed to parse JSON for Uri: [{uri}]. Exception: {ex}"); + return new SearchResult(ExternalProviderType.Lrclib); + } + + if (response == null) + { + _logger?.LogWarning($"Lrclib. Empty parsed response for Uri: [{uri}]"); + return new SearchResult(ExternalProviderType.Lrclib); + } + + if (response.Code == 404 || string.Equals(response.Name, "TrackNotFound", StringComparison.OrdinalIgnoreCase)) + { + _logger?.LogInformation($"Lrclib. Track not found for Uri: [{uri}]"); + return new SearchResult(ExternalProviderType.Lrclib, ResponseStatusCode.NoDataFound); + } + + if (response.Instrumental) + return new SearchResult(ExternalProviderType.Lrclib).AddInstrumental(true); + + if (string.IsNullOrWhiteSpace(response.PlainLyrics)) + { + _logger?.LogWarning($"Lrclib. Can't find lyrics for Uri: [{uri}]"); + return new SearchResult(ExternalProviderType.Lrclib); + } + + var result = Parser.Parse(response.PlainLyrics); + return new SearchResult(result, ExternalProviderType.Lrclib); + } + } +} diff --git a/LyricsScraperNET/Providers/Lrclib/LrclibTrackResponse.cs b/LyricsScraperNET/Providers/Lrclib/LrclibTrackResponse.cs new file mode 100644 index 0000000..a550119 --- /dev/null +++ b/LyricsScraperNET/Providers/Lrclib/LrclibTrackResponse.cs @@ -0,0 +1,19 @@ +using System.Text.Json.Serialization; + +namespace LyricsScraperNET.Providers.Lrclib +{ + internal sealed class LrclibTrackResponse + { + [JsonPropertyName("instrumental")] + public bool Instrumental { get; set; } + + [JsonPropertyName("plainLyrics")] + public string? PlainLyrics { get; set; } + + [JsonPropertyName("code")] + public int? Code { get; set; } + + [JsonPropertyName("name")] + public string? Name { get; set; } + } +} diff --git a/LyricsScraperNET/Providers/Lrclib/LrclibUriConverter.cs b/LyricsScraperNET/Providers/Lrclib/LrclibUriConverter.cs new file mode 100644 index 0000000..2846c5f --- /dev/null +++ b/LyricsScraperNET/Providers/Lrclib/LrclibUriConverter.cs @@ -0,0 +1,21 @@ +using LyricsScraperNET.Providers.Abstract; +using System; + +namespace LyricsScraperNET.Providers.Lrclib +{ + internal sealed class LrclibUriConverter : IExternalUriConverter + { + internal const string BaseApiUrl = "https://lrclib.net/api/get"; + + public Uri GetArtistUri(string artist) + { + throw new NotImplementedException(); + } + + public Uri GetLyricUri(string artist, string song) + { + var query = $"artist_name={Uri.EscapeDataString(artist)}&track_name={Uri.EscapeDataString(song)}"; + return new Uri($"{BaseApiUrl}?{query}"); + } + } +} diff --git a/LyricsScraperNET/Providers/LyricFind/LyricFindProvider.cs b/LyricsScraperNET/Providers/LyricFind/LyricFindProvider.cs index 7f8e462..da66447 100644 --- a/LyricsScraperNET/Providers/LyricFind/LyricFindProvider.cs +++ b/LyricsScraperNET/Providers/LyricFind/LyricFindProvider.cs @@ -136,7 +136,7 @@ private SearchResult PostProcessLyric(Uri uri, string text) } // Trim the beginning of the text to the lyrics - text = text.Substring(startIndex + _lyricStart.Length + 1); + text = text[(startIndex + _lyricStart.Length + 1)..]; // Finding the end of the lyric text in the json field value. int start = text.IndexOf("\"") + 1; @@ -154,7 +154,7 @@ private SearchResult PostProcessLyric(Uri uri, string text) return new SearchResult(Models.ExternalProviderType.LyricFind); } - string result = Parser.Parse(text.Substring(start, endOfLyricInJson - start)); + string result = Parser.Parse(text[start..endOfLyricInJson]); return new SearchResult(result, Models.ExternalProviderType.LyricFind); } @@ -163,7 +163,7 @@ private SearchResult PostProcessLyric(Uri uri, string text) /// /// Check if lyric text contains region restricted information like viewable (false) and repsonse with code (206) and description. /// - private bool IsRegionRestrictedLyric(string text) + private static bool IsRegionRestrictedLyric(string text) { return TryReturnBooleanFieldValue(text, _viewableStart, "false") && Regex.IsMatch(text, _lyricNotAvailablePattern); @@ -172,7 +172,7 @@ private bool IsRegionRestrictedLyric(string text) /// /// Check if lyric text contains instrumental flag. /// - private bool IsInstumentalLyric(string text) + private static bool IsInstumentalLyric(string text) { return TryReturnBooleanFieldValue(text, _instrumentalStart) || TryReturnBooleanFieldValue(text, _songIsInstrumentalStart); @@ -182,13 +182,13 @@ private bool IsInstumentalLyric(string text) /// Try to find and return the fielad value as boolean. Pattern: [:true(or false)]. /// In case if fieldName is not found returns false. /// - private bool TryReturnBooleanFieldValue(string text, string fieldName, string booleanValue = "true") + private static bool TryReturnBooleanFieldValue(string text, string fieldName, string booleanValue = "true") { var startIndex = text.IndexOf(fieldName); if (startIndex <= 0) return false; var fieldValue = text.Substring(startIndex + fieldName.Length + 1, 5); - return fieldValue.IndexOf(booleanValue, StringComparison.OrdinalIgnoreCase) >= 0; + return fieldValue.Contains(booleanValue, StringComparison.OrdinalIgnoreCase); } } } diff --git a/LyricsScraperNET/Providers/LyricsFreak/LyricsFreakOptions.cs b/LyricsScraperNET/Providers/LyricsFreak/LyricsFreakOptions.cs index 9af00e1..442fd66 100644 --- a/LyricsScraperNET/Providers/LyricsFreak/LyricsFreakOptions.cs +++ b/LyricsScraperNET/Providers/LyricsFreak/LyricsFreakOptions.cs @@ -22,12 +22,7 @@ public override bool Equals(object? obj) public override int GetHashCode() { - unchecked - { - int hash = 17; - hash = (hash * 31) + ExternalProviderType.GetHashCode(); - return hash; - } + return System.HashCode.Combine(ExternalProviderType); } } } diff --git a/LyricsScraperNET/Providers/LyricsFreak/LyricsFreakProvider.cs b/LyricsScraperNET/Providers/LyricsFreak/LyricsFreakProvider.cs index 4f29224..d9f2f62 100644 --- a/LyricsScraperNET/Providers/LyricsFreak/LyricsFreakProvider.cs +++ b/LyricsScraperNET/Providers/LyricsFreak/LyricsFreakProvider.cs @@ -139,7 +139,7 @@ public override void WithLogger(ILoggerFactory loggerFactory) #region Private methods - private string GetSongHrefFromHtmlBody(string htmlBody, string song) + private static string GetSongHrefFromHtmlBody(string htmlBody, string song) { // Encoded needed for songs like "Devil's Calling". Title in htmlBody will be: "Devil's Calling Lyrics" string formattedXPath = string.Format(LyricsHrefXPath, GetEncodedSong(song)); @@ -160,7 +160,7 @@ private string GetSongHrefFromHtmlBody(string htmlBody, string song) return hrefSong; } - private string GetSongLyricsFromHtmlBody(string htmlBody) + private static string GetSongLyricsFromHtmlBody(string htmlBody) { var lyricsNode = htmlBody.SelectSingleNodeByXPath(LyricsDivXPath); @@ -171,7 +171,7 @@ private string GetSongLyricsFromHtmlBody(string htmlBody) return lyricsText; } - private string GetEncodedSong(string song) + private static string GetEncodedSong(string song) { string encodedSong = System.Net.WebUtility.HtmlEncode(song).ToLowerInvariant(); encodedSong = encodedSong.Replace("'", "'"); diff --git a/LyricsScraperNET/Providers/Models/ExternalProviderType.cs b/LyricsScraperNET/Providers/Models/ExternalProviderType.cs index 69d5c44..e2cecf4 100644 --- a/LyricsScraperNET/Providers/Models/ExternalProviderType.cs +++ b/LyricsScraperNET/Providers/Models/ExternalProviderType.cs @@ -9,6 +9,7 @@ public enum ExternalProviderType SongLyrics, LyricFind, KPopLyrics, - LyricsFreak + LyricsFreak, + Lrclib } } diff --git a/LyricsScraperNET/Providers/SongLyrics/SongLyricsOptions.cs b/LyricsScraperNET/Providers/SongLyrics/SongLyricsOptions.cs index d67cdc3..abe12e4 100644 --- a/LyricsScraperNET/Providers/SongLyrics/SongLyricsOptions.cs +++ b/LyricsScraperNET/Providers/SongLyrics/SongLyricsOptions.cs @@ -22,12 +22,7 @@ public override bool Equals(object? obj) public override int GetHashCode() { - unchecked - { - int hash = 17; - hash = (hash * 31) + ExternalProviderType.GetHashCode(); - return hash; - } + return System.HashCode.Combine(ExternalProviderType); } } } diff --git a/README.md b/README.md index 2142986..25f309d 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,7 @@ The library currently supports the following providers: - [LyricFind](https://www.lyricfind.com/) - [LyricsFreak](https://www.lyricsfreak.com/) (added by [@ajay201402](https://github.com/ajay201402)) - [kpoplyrics](https://www.kpoplyrics.net/) (added by [@Lukeuke](https://github.com/Lukeuke)) +- [LRCLIB](https://lrclib.net/) - [Letras.mus.br](https://www.letras.mus.br/) (**Coming soon** 🚧. [Issue #40](https://github.com/skuill/LyricsScraperNET/issues/40)) - [darklyrics](http://www.darklyrics.com/) (**Coming soon** 🚧. [Issue #41](https://github.com/skuill/LyricsScraperNET/issues/41)) - [vagalume](https://www.vagalume.com.br/) (**Coming soon** 🚧. [Issue #42](https://github.com/skuill/LyricsScraperNET/issues/42)) diff --git a/Tests/LyricsScraperNET.IntegrationTest/LyricsScraperNET.IntegrationTest.csproj b/Tests/LyricsScraperNET.IntegrationTest/LyricsScraperNET.IntegrationTest.csproj index 86d9133..d77098f 100644 --- a/Tests/LyricsScraperNET.IntegrationTest/LyricsScraperNET.IntegrationTest.csproj +++ b/Tests/LyricsScraperNET.IntegrationTest/LyricsScraperNET.IntegrationTest.csproj @@ -105,6 +105,12 @@ PreserveNewest + + PreserveNewest + + + PreserveNewest +
diff --git a/Tests/LyricsScraperNET.IntegrationTest/Providers/Lrclib/LrclibProviderTest.cs b/Tests/LyricsScraperNET.IntegrationTest/Providers/Lrclib/LrclibProviderTest.cs new file mode 100644 index 0000000..dfc11b5 --- /dev/null +++ b/Tests/LyricsScraperNET.IntegrationTest/Providers/Lrclib/LrclibProviderTest.cs @@ -0,0 +1,107 @@ +using LyricsScraperNET.Models.Requests; +using LyricsScraperNET.Models.Responses; +using LyricsScraperNET.Providers.Lrclib; +using LyricsScraperNET.Providers.Models; +using LyricsScraperNET.TestShared.Providers; +using LyricsScraperNET.TestShared.TestModel; +using System.Threading; +using System.Threading.Tasks; +using Xunit; + +namespace LyricsScraperNET.IntegrationTest.Providers.Lrclib +{ + public class LrclibProviderTest : ProviderTestBase + { + #region sync + + [Theory] + [MemberData(nameof(GetTestData), arguments: "Providers\\Lrclib\\lyric_test_data.json")] + public void SearchLyric_IntegrationDynamicData_Success(LyricsTestData testData) + { + // Arrange + var lyricsClient = new LrclibProvider(); + SearchRequest searchRequest = CreateSearchRequest(testData); + CancellationToken cancellationToken = CancellationToken.None; + + // Act + var searchResult = lyricsClient.SearchLyric(searchRequest, cancellationToken); + + // Assert + Assert.NotNull(searchResult); + Assert.False(searchResult.IsEmpty()); + Assert.Equal(ResponseStatusCode.Success, searchResult.ResponseStatusCode); + Assert.True(string.IsNullOrEmpty(searchResult.ResponseMessage)); + Assert.Equal(ExternalProviderType.Lrclib, searchResult.ExternalProviderType); + Assert.Equal(testData.LyricResultData.Replace("\r\n", "\n"), searchResult.LyricText.Replace("\r\n", "\n")); + Assert.False(searchResult.Instrumental); + } + + [Theory] + [InlineData("asdfasdfasdfasdf", "asdfasdfasdfasdf")] + public void SearchLyric_NotExistsLyrics_ShouldReturnNoDataFoundStatus(string artist, string song) + { + // Arrange + var lyricsClient = new LrclibProvider(); + var searchRequest = new ArtistAndSongSearchRequest(artist, song); + CancellationToken cancellationToken = CancellationToken.None; + + // Act + var searchResult = lyricsClient.SearchLyric(searchRequest, cancellationToken); + + // Assert + Assert.NotNull(searchResult); + Assert.True(searchResult.IsEmpty()); + Assert.Equal(ResponseStatusCode.NoDataFound, searchResult.ResponseStatusCode); + Assert.Equal(ExternalProviderType.Lrclib, searchResult.ExternalProviderType); + Assert.True(string.IsNullOrEmpty(searchResult.ResponseMessage)); + Assert.False(searchResult.Instrumental); + } + + #endregion + + #region async + + [Theory] + [MemberData(nameof(GetTestData), arguments: "Providers\\Lrclib\\lyric_test_data.json")] + public async Task SearchLyricAsync_IntegrationDynamicData_Success(LyricsTestData testData) + { + // Arrange + var lyricsClient = new LrclibProvider(); + SearchRequest searchRequest = CreateSearchRequest(testData); + + // Act + var searchResult = await lyricsClient.SearchLyricAsync(searchRequest, TestContext.Current.CancellationToken); + + // Assert + Assert.NotNull(searchResult); + Assert.False(searchResult.IsEmpty()); + Assert.Equal(ResponseStatusCode.Success, searchResult.ResponseStatusCode); + Assert.True(string.IsNullOrEmpty(searchResult.ResponseMessage)); + Assert.Equal(ExternalProviderType.Lrclib, searchResult.ExternalProviderType); + Assert.Equal(testData.LyricResultData.Replace("\r\n", "\n"), searchResult.LyricText.Replace("\r\n", "\n")); + Assert.False(searchResult.Instrumental); + } + + [Theory] + [InlineData("asdfasdfasdfasdf", "asdfasdfasdfasdf")] + public async Task SearchLyricAsync_NotExistsLyrics_ShouldReturnNoDataFoundStatus(string artist, string song) + { + // Arrange + var lyricsClient = new LrclibProvider(); + var searchRequest = new ArtistAndSongSearchRequest(artist, song); + + // Act + var searchResult = await lyricsClient.SearchLyricAsync(searchRequest, TestContext.Current.CancellationToken); + + // Assert + Assert.NotNull(searchResult); + Assert.True(searchResult.IsEmpty()); + Assert.Equal(ResponseStatusCode.NoDataFound, searchResult.ResponseStatusCode); + Assert.Equal(ExternalProviderType.Lrclib, searchResult.ExternalProviderType); + Assert.True(string.IsNullOrEmpty(searchResult.ResponseMessage)); + Assert.False(searchResult.Instrumental); + } + + #endregion + } +} diff --git a/Tests/LyricsScraperNET.IntegrationTest/Providers/Lrclib/Resources/Lyrics_Result_01.txt b/Tests/LyricsScraperNET.IntegrationTest/Providers/Lrclib/Resources/Lyrics_Result_01.txt new file mode 100644 index 0000000..a67e450 --- /dev/null +++ b/Tests/LyricsScraperNET.IntegrationTest/Providers/Lrclib/Resources/Lyrics_Result_01.txt @@ -0,0 +1,36 @@ +Now your heroes have fallen +Championless, the seas are rising +So torch every banner +Every hope of surviving +This storm is breaking +Security has left you treading water + +Now taste the fear +Taste the uncertainty +What will you do (what will you do) +When there's nothing left for you to cling to? +What will you do (what will you do) +With your one last breath? + +Thrive in your emptiness +Burn all you love +There's no hope for the weak +Our heroes have died + +No heart, no hope +Face to face with the abyss (with the abyss) +One by one they fall away and won't be missed + +Can you hear it? +Can you hear the sound? +As our broken idols +Come crashing down +Now taste the fear +Now taste the fear + +Burn all you love +There's no hope for the weak +Our heroes have died +Burn all you love +There's no hope for the weak +Burn all you love \ No newline at end of file diff --git a/Tests/LyricsScraperNET.IntegrationTest/Providers/Lrclib/lyric_test_data.json b/Tests/LyricsScraperNET.IntegrationTest/Providers/Lrclib/lyric_test_data.json new file mode 100644 index 0000000..3efad64 --- /dev/null +++ b/Tests/LyricsScraperNET.IntegrationTest/Providers/Lrclib/lyric_test_data.json @@ -0,0 +1,14 @@ +[ + { + "LyricResultPath": "Providers/Lrclib/Resources/Lyrics_Result_01.txt", + "ArtistName": "Parkway Drive", + "SongName": "Idols and Anchors", + "SongUri": null + }, + { + "LyricResultPath": "Providers/Lrclib/Resources/Lyrics_Result_01.txt", + "ArtistName": null, + "SongName": null, + "SongUri": "https://lrclib.net/api/get?artist_name=Parkway+Drive&track_name=Idols+and+Anchors" + } +] diff --git a/Tests/LyricsScraperNET.UnitTest/Configuration/ServiceCollectionExtensionsTest.cs b/Tests/LyricsScraperNET.UnitTest/Configuration/ServiceCollectionExtensionsTest.cs index 2cc8d8a..fb9ba32 100644 --- a/Tests/LyricsScraperNET.UnitTest/Configuration/ServiceCollectionExtensionsTest.cs +++ b/Tests/LyricsScraperNET.UnitTest/Configuration/ServiceCollectionExtensionsTest.cs @@ -69,7 +69,8 @@ public void IocContainer_GetService_LyricsScraperClient_FullSetup() { ExternalProviderType.SongLyrics, 44}, { ExternalProviderType.LyricFind, 55}, { ExternalProviderType.KPopLyrics, 66}, - { ExternalProviderType.LyricsFreak, 77} + { ExternalProviderType.LyricsFreak, 77}, + { ExternalProviderType.Lrclib, 88} }; string settingsPath = "Resources\\full_test_settings.json"; diff --git a/Tests/LyricsScraperNET.UnitTest/Extensions/LyricsScraperClientExtensionsTest.cs b/Tests/LyricsScraperNET.UnitTest/Extensions/LyricsScraperClientExtensionsTest.cs index f3d2285..938fd75 100644 --- a/Tests/LyricsScraperNET.UnitTest/Extensions/LyricsScraperClientExtensionsTest.cs +++ b/Tests/LyricsScraperNET.UnitTest/Extensions/LyricsScraperClientExtensionsTest.cs @@ -115,6 +115,21 @@ public void LyricsScraperClient_WithLyricFind_ReturnsIsEnabled() Assert.Equal(Constants.ProvidersSearchPriorities[ExternalProviderType.LyricFind], externalTypeProvider.SearchPriority); } + [Fact] + public void LyricsScraperClient_WithLrclib_ReturnsIsEnabled() + { + // Act + var lyricsScraperClient = _lyricsScraperClient.WithLrclib(); + var externalTypeProvider = lyricsScraperClient[ExternalProviderType.Lrclib]; + + // Assert + Assert.NotNull(lyricsScraperClient); + Assert.True(lyricsScraperClient.IsEnabled); + Assert.NotNull(externalTypeProvider); + Assert.True(externalTypeProvider.IsEnabled); + Assert.Equal(Constants.ProvidersSearchPriorities[ExternalProviderType.Lrclib], externalTypeProvider.SearchPriority); + } + [Fact] public void LyricsScraperClient_WithAllProviders_ReturnsIsEnabled() { diff --git a/Tests/LyricsScraperNET.UnitTest/LyricsScraperClientTests.cs b/Tests/LyricsScraperNET.UnitTest/LyricsScraperClientTests.cs index 459ccc7..ca545ad 100644 --- a/Tests/LyricsScraperNET.UnitTest/LyricsScraperClientTests.cs +++ b/Tests/LyricsScraperNET.UnitTest/LyricsScraperClientTests.cs @@ -445,7 +445,11 @@ public async Task SearchLyricAsync_WithUseParallelSearchEnabled_ShouldReturnFirs var fastProvider = A.Fake(); A.CallTo(() => fastProvider.SearchLyricAsync(A._, A._)) - .Returns(fastResult); + .ReturnsLazily(async (SearchRequest r, CancellationToken ct) => + { + await Task.Delay(1000, ct); // Simulate a fast execution. + return fastResult; + }); ; A.CallTo(() => fastProvider.IsEnabled).Returns(true); // Create slow providers that simulate delayed response. diff --git a/Tests/LyricsScraperNET.UnitTest/LyricsScraperNET.UnitTest.csproj b/Tests/LyricsScraperNET.UnitTest/LyricsScraperNET.UnitTest.csproj index 6b6cb46..5aed30e 100644 --- a/Tests/LyricsScraperNET.UnitTest/LyricsScraperNET.UnitTest.csproj +++ b/Tests/LyricsScraperNET.UnitTest/LyricsScraperNET.UnitTest.csproj @@ -123,6 +123,27 @@ PreserveNewest + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest +
diff --git a/Tests/LyricsScraperNET.UnitTest/Providers/Lrclib/LrclibProviderTest.cs b/Tests/LyricsScraperNET.UnitTest/Providers/Lrclib/LrclibProviderTest.cs new file mode 100644 index 0000000..90a476f --- /dev/null +++ b/Tests/LyricsScraperNET.UnitTest/Providers/Lrclib/LrclibProviderTest.cs @@ -0,0 +1,83 @@ +using LyricsScraperNET.Models.Requests; +using LyricsScraperNET.Models.Responses; +using LyricsScraperNET.Providers.Lrclib; +using LyricsScraperNET.Providers.Models; +using LyricsScraperNET.TestShared.Extensions; +using LyricsScraperNET.TestShared.Providers; +using LyricsScraperNET.TestShared.TestModel; +using System.Threading; +using Xunit; + +namespace LyricsScraperNET.UnitTest.Providers.Lrclib +{ + public class LrclibProviderTest : ProviderTestBase + { + [Theory] + [MemberData(nameof(GetTestData), arguments: "Providers\\Lrclib\\lyric_test_data.json")] + public void SearchLyric_UnitDynamicData_Success(LyricsTestData testData) + { + // Arrange + var lyricsClient = new LrclibProvider(); + lyricsClient.ConfigureExternalProvider(testData); + + SearchRequest searchRequest = CreateSearchRequest(testData); + CancellationToken cancellationToken = CancellationToken.None; + + // Act + var searchResult = lyricsClient.SearchLyric(searchRequest, cancellationToken); + + // Assert + Assert.NotNull(searchResult); + Assert.Equal(ResponseStatusCode.Success, searchResult.ResponseStatusCode); + Assert.True(string.IsNullOrEmpty(searchResult.ResponseMessage)); + Assert.Equal(ExternalProviderType.Lrclib, searchResult.ExternalProviderType); + Assert.Equal(testData.LyricResultData.Replace("\r\n", "\n"), searchResult.LyricText.Replace("\r\n", "\n")); + Assert.False(searchResult.Instrumental); + } + + [Theory] + [MemberData(nameof(GetTestData), arguments: "Providers\\Lrclib\\instrumental_test_data.json")] + public void SearchLyric_UnitDynamicData_Instrumental(LyricsTestData testData) + { + // Arrange + var lyricsClient = new LrclibProvider(); + lyricsClient.ConfigureExternalProvider(testData); + + SearchRequest searchRequest = CreateSearchRequest(testData); + CancellationToken cancellationToken = CancellationToken.None; + + // Act + var searchResult = lyricsClient.SearchLyric(searchRequest, cancellationToken); + + // Assert + Assert.NotNull(searchResult); + Assert.True(searchResult.IsEmpty()); + Assert.Equal(ResponseStatusCode.Success, searchResult.ResponseStatusCode); + Assert.True(string.IsNullOrEmpty(searchResult.ResponseMessage)); + Assert.Equal(ExternalProviderType.Lrclib, searchResult.ExternalProviderType); + Assert.True(searchResult.Instrumental); + } + + [Theory] + [MemberData(nameof(GetTestData), arguments: "Providers\\Lrclib\\not_found_test_data.json")] + public void SearchLyric_UnitDynamicData_NotFound(LyricsTestData testData) + { + // Arrange + var lyricsClient = new LrclibProvider(); + lyricsClient.ConfigureExternalProvider(testData); + + SearchRequest searchRequest = CreateSearchRequest(testData); + + // Act + var searchResult = lyricsClient.SearchLyric(searchRequest, TestContext.Current.CancellationToken); + + // Assert + Assert.NotNull(searchResult); + Assert.True(searchResult.IsEmpty()); + Assert.Equal(ResponseStatusCode.NoDataFound, searchResult.ResponseStatusCode); + Assert.Equal(ExternalProviderType.Lrclib, searchResult.ExternalProviderType); + Assert.True(string.IsNullOrEmpty(searchResult.ResponseMessage)); + Assert.False(searchResult.Instrumental); + } + } +} diff --git a/Tests/LyricsScraperNET.UnitTest/Providers/Lrclib/LrclibUriConverterTests.cs b/Tests/LyricsScraperNET.UnitTest/Providers/Lrclib/LrclibUriConverterTests.cs new file mode 100644 index 0000000..cbb931e --- /dev/null +++ b/Tests/LyricsScraperNET.UnitTest/Providers/Lrclib/LrclibUriConverterTests.cs @@ -0,0 +1,25 @@ +using LyricsScraperNET.Providers.Lrclib; +using System; +using Xunit; + +namespace LyricsScraperNET.UnitTest.Providers.Lrclib +{ + public class LrclibUriConverterTests + { + [Theory] + [InlineData("Parkway Drive", "Idols and Anchors", "https://lrclib.net/api/get?artist_name=Parkway%20Drive&track_name=Idols%20and%20Anchors")] + [InlineData("Borislav Slavov", "I Want to Live", "https://lrclib.net/api/get?artist_name=Borislav%20Slavov&track_name=I%20Want%20to%20Live")] + [InlineData("Of Mice & Men", "You're Not Alone", "https://lrclib.net/api/get?artist_name=Of%20Mice%20%26%20Men&track_name=You%27re%20Not%20Alone")] + public void GetLyricUri_MultipleInputs_ShouldBeParse(string artistName, string songName, string expectedUri) + { + // Arrange + var uriConverter = new LrclibUriConverter(); + + // Act + var actual = uriConverter.GetLyricUri(artistName, songName); + + // Assert + Assert.Equal(new Uri(expectedUri), actual); + } + } +} diff --git a/Tests/LyricsScraperNET.UnitTest/Providers/Lrclib/Resources/Instrumental_Json_01.txt b/Tests/LyricsScraperNET.UnitTest/Providers/Lrclib/Resources/Instrumental_Json_01.txt new file mode 100644 index 0000000..1d54c73 --- /dev/null +++ b/Tests/LyricsScraperNET.UnitTest/Providers/Lrclib/Resources/Instrumental_Json_01.txt @@ -0,0 +1 @@ +{"id":624237,"name":"YYZ","trackName":"YYZ","artistName":"Rush","albumName":"Moving Pictures","duration":266.0,"instrumental":true,"plainLyrics":null,"syncedLyrics":null,"lyricsfile":"version: '1.0'\nmetadata:\n title: YYZ\n artist: Rush\n album: Moving Pictures\n duration_ms: 266000\n instrumental: true\nlines: []\n"} diff --git a/Tests/LyricsScraperNET.UnitTest/Providers/Lrclib/Resources/Lyrics_Json_01.txt b/Tests/LyricsScraperNET.UnitTest/Providers/Lrclib/Resources/Lyrics_Json_01.txt new file mode 100644 index 0000000..eda8908 --- /dev/null +++ b/Tests/LyricsScraperNET.UnitTest/Providers/Lrclib/Resources/Lyrics_Json_01.txt @@ -0,0 +1 @@ +{"id":1166228,"name":"Idols and Anchors","trackName":"Idols and Anchors","artistName":"Parkway Drive","albumName":"Horizons","duration":228.0,"instrumental":false,"plainLyrics":"Now your heroes have fallen\nChampionless, the seas are rising\nSo torch every banner\nEvery hope of surviving\nThis storm is breaking\nSecurity has left you treading water\n\nNow taste the fear\nTaste the uncertainty\nWhat will you do (what will you do)\nWhen there's nothing left for you to cling to?\nWhat will you do (what will you do)\nWith your one last breath?\n\nThrive in your emptiness\nBurn all you love\nThere's no hope for the weak\nOur heroes have died\n\nNo heart, no hope\nFace to face with the abyss (with the abyss)\nOne by one they fall away and won't be missed\n\nCan you hear it?\nCan you hear the sound?\nAs our broken idols\nCome crashing down\nNow taste the fear\nNow taste the fear\n\nBurn all you love\nThere's no hope for the weak\nOur heroes have died\nBurn all you love\nThere's no hope for the weak\nBurn all you love","syncedLyrics":"[00:48.59] Now your heroes have fallen\n[00:53.53] Championless, the seas are rising\n[00:58.08] So torch every banner\n[01:00.31] Every hope of surviving\n[01:03.06] This storm is breaking\n[01:05.05] Security has left you treading water\n[01:09.46] Now taste the fear\n[01:15.22] Taste the uncertainty\n[01:17.75] What will you do (what will you do)\n[01:20.34] When there's nothing left for you to cling to?\n[01:23.22] What will you do (what will you do)\n[01:25.94] With your one last breath?\n[01:32.40] Thrive in your emptiness\n[01:37.80] Burn all you love\n[01:41.01] There's no hope for the weak\n[01:43.82] Our heroes have died\n[01:47.28] \n[01:49.74] No heart, no hope\n[02:02.19] Face to face with the abyss (with the abyss)\n[02:06.78] One by one they fall away and won't be missed\n[02:11.67] Can you hear it?\n[02:14.25] Can you hear the sound?\n[02:17.30] As our broken idols\n[02:21.94] Come crashing down\n[02:27.24] Now taste the fear\n[02:31.77] Now taste the fear\n[02:38.70] Burn all you love\n[02:41.68] There's no hope for the weak\n[02:44.80] Our heroes have died\n[02:50.69] Burn all you love\n[02:53.46] There's no hope for the weak\n[02:56.84] Burn all you love\n[03:03.19] ","lyricsfile":"version: '1.0'\nmetadata:\n title: Idols and Anchors\n artist: Parkway Drive\n album: Horizons\n duration_ms: 228000\n instrumental: false\nlines:\n- text: Now your heroes have fallen\n start_ms: 48590\n end_ms: 53530\n- text: Championless, the seas are rising\n start_ms: 53530\n end_ms: 58080\n- text: So torch every banner\n start_ms: 58080\n end_ms: 60310\n- text: Every hope of surviving\n start_ms: 60310\n end_ms: 63060\n- text: This storm is breaking\n start_ms: 63060\n end_ms: 65050\n- text: Security has left you treading water\n start_ms: 65050\n end_ms: 69460\n- text: Now taste the fear\n start_ms: 69460\n end_ms: 75220\n- text: Taste the uncertainty\n start_ms: 75220\n end_ms: 77750\n- text: What will you do (what will you do)\n start_ms: 77750\n end_ms: 80340\n- text: When there's nothing left for you to cling to?\n start_ms: 80340\n end_ms: 83220\n- text: What will you do (what will you do)\n start_ms: 83220\n end_ms: 85940\n- text: With your one last breath?\n start_ms: 85940\n end_ms: 92400\n- text: Thrive in your emptiness\n start_ms: 92400\n end_ms: 97800\n- text: Burn all you love\n start_ms: 97800\n end_ms: 101010\n- text: There's no hope for the weak\n start_ms: 101010\n end_ms: 103820\n- text: Our heroes have died\n start_ms: 103820\n end_ms: 107280\n- text: ''\n start_ms: 107280\n end_ms: 109740\n- text: No heart, no hope\n start_ms: 109740\n end_ms: 122190\n- text: Face to face with the abyss (with the abyss)\n start_ms: 122190\n end_ms: 126780\n- text: One by one they fall away and won't be missed\n start_ms: 126780\n end_ms: 131670\n- text: Can you hear it?\n start_ms: 131670\n end_ms: 134250\n- text: Can you hear the sound?\n start_ms: 134250\n end_ms: 137300\n- text: As our broken idols\n start_ms: 137300\n end_ms: 141940\n- text: Come crashing down\n start_ms: 141940\n end_ms: 147240\n- text: Now taste the fear\n start_ms: 147240\n end_ms: 151770\n- text: Now taste the fear\n start_ms: 151770\n end_ms: 158700\n- text: Burn all you love\n start_ms: 158700\n end_ms: 161680\n- text: There's no hope for the weak\n start_ms: 161680\n end_ms: 164800\n- text: Our heroes have died\n start_ms: 164800\n end_ms: 170690\n- text: Burn all you love\n start_ms: 170690\n end_ms: 173460\n- text: There's no hope for the weak\n start_ms: 173460\n end_ms: 176840\n- text: Burn all you love\n start_ms: 176840\n end_ms: 183190\n- text: ''\n start_ms: 183190\nplain: |-\n Now your heroes have fallen\n Championless, the seas are rising\n So torch every banner\n Every hope of surviving\n This storm is breaking\n Security has left you treading water\n\n Now taste the fear\n Taste the uncertainty\n What will you do (what will you do)\n When there's nothing left for you to cling to?\n What will you do (what will you do)\n With your one last breath?\n\n Thrive in your emptiness\n Burn all you love\n There's no hope for the weak\n Our heroes have died\n\n No heart, no hope\n Face to face with the abyss (with the abyss)\n One by one they fall away and won't be missed\n\n Can you hear it?\n Can you hear the sound?\n As our broken idols\n Come crashing down\n Now taste the fear\n Now taste the fear\n\n Burn all you love\n There's no hope for the weak\n Our heroes have died\n Burn all you love\n There's no hope for the weak\n Burn all you love\n"} diff --git a/Tests/LyricsScraperNET.UnitTest/Providers/Lrclib/Resources/Lyrics_Result_01.txt b/Tests/LyricsScraperNET.UnitTest/Providers/Lrclib/Resources/Lyrics_Result_01.txt new file mode 100644 index 0000000..a67e450 --- /dev/null +++ b/Tests/LyricsScraperNET.UnitTest/Providers/Lrclib/Resources/Lyrics_Result_01.txt @@ -0,0 +1,36 @@ +Now your heroes have fallen +Championless, the seas are rising +So torch every banner +Every hope of surviving +This storm is breaking +Security has left you treading water + +Now taste the fear +Taste the uncertainty +What will you do (what will you do) +When there's nothing left for you to cling to? +What will you do (what will you do) +With your one last breath? + +Thrive in your emptiness +Burn all you love +There's no hope for the weak +Our heroes have died + +No heart, no hope +Face to face with the abyss (with the abyss) +One by one they fall away and won't be missed + +Can you hear it? +Can you hear the sound? +As our broken idols +Come crashing down +Now taste the fear +Now taste the fear + +Burn all you love +There's no hope for the weak +Our heroes have died +Burn all you love +There's no hope for the weak +Burn all you love \ No newline at end of file diff --git a/Tests/LyricsScraperNET.UnitTest/Providers/Lrclib/Resources/NotFound_Json_01.txt b/Tests/LyricsScraperNET.UnitTest/Providers/Lrclib/Resources/NotFound_Json_01.txt new file mode 100644 index 0000000..1ffa6a1 --- /dev/null +++ b/Tests/LyricsScraperNET.UnitTest/Providers/Lrclib/Resources/NotFound_Json_01.txt @@ -0,0 +1 @@ +{"code":404,"name":"TrackNotFound","message":"Failed to find specified track"} diff --git a/Tests/LyricsScraperNET.UnitTest/Providers/Lrclib/instrumental_test_data.json b/Tests/LyricsScraperNET.UnitTest/Providers/Lrclib/instrumental_test_data.json new file mode 100644 index 0000000..d10ce9b --- /dev/null +++ b/Tests/LyricsScraperNET.UnitTest/Providers/Lrclib/instrumental_test_data.json @@ -0,0 +1,9 @@ +[ + { + "LyricPagePath": "Providers/Lrclib/Resources/Instrumental_Json_01.txt", + "LyricResultPath": null, + "ArtistName": "Rush", + "SongName": "YYZ", + "SongUri": null + } +] diff --git a/Tests/LyricsScraperNET.UnitTest/Providers/Lrclib/lyric_test_data.json b/Tests/LyricsScraperNET.UnitTest/Providers/Lrclib/lyric_test_data.json new file mode 100644 index 0000000..75de62a --- /dev/null +++ b/Tests/LyricsScraperNET.UnitTest/Providers/Lrclib/lyric_test_data.json @@ -0,0 +1,9 @@ +[ + { + "LyricPagePath": "Providers/Lrclib/Resources/Lyrics_Json_01.txt", + "LyricResultPath": "Providers/Lrclib/Resources/Lyrics_Result_01.txt", + "ArtistName": "Parkway Drive", + "SongName": "Idols and Anchors", + "SongUri": null + } +] diff --git a/Tests/LyricsScraperNET.UnitTest/Providers/Lrclib/not_found_test_data.json b/Tests/LyricsScraperNET.UnitTest/Providers/Lrclib/not_found_test_data.json new file mode 100644 index 0000000..50c4455 --- /dev/null +++ b/Tests/LyricsScraperNET.UnitTest/Providers/Lrclib/not_found_test_data.json @@ -0,0 +1,9 @@ +[ + { + "LyricPagePath": "Providers/Lrclib/Resources/NotFound_Json_01.txt", + "LyricResultPath": null, + "ArtistName": "asdfasdfasdfasdf", + "SongName": "asdfasdfasdfasdf", + "SongUri": null + } +] diff --git a/Tests/LyricsScraperNET.UnitTest/Resources/full_test_settings.json b/Tests/LyricsScraperNET.UnitTest/Resources/full_test_settings.json index 635a9a4..ff4ec0c 100644 --- a/Tests/LyricsScraperNET.UnitTest/Resources/full_test_settings.json +++ b/Tests/LyricsScraperNET.UnitTest/Resources/full_test_settings.json @@ -30,6 +30,10 @@ "LyricsFreakOptions": { "SearchPriority": 77, "Enabled": true + }, + "LrclibOptions": { + "SearchPriority": 88, + "Enabled": true } } } \ No newline at end of file From 50a83bb46a0860fdace65fa82865630c9ac08218 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=92=D0=B8=D1=82=D0=B0=D0=BB=D0=B8=D0=B9=20=D0=9A=D0=B2?= =?UTF-8?q?=D0=B0=D1=88=D0=B8=D0=BD?= Date: Sun, 30 Aug 2026 20:45:14 +0200 Subject: [PATCH 03/11] Updated cicd.yaml to latest --- .github/workflows/cicd.yaml | 63 +++++++++++++++++-------------------- 1 file changed, 29 insertions(+), 34 deletions(-) diff --git a/.github/workflows/cicd.yaml b/.github/workflows/cicd.yaml index b7da333..6f6253a 100644 --- a/.github/workflows/cicd.yaml +++ b/.github/workflows/cicd.yaml @@ -1,38 +1,34 @@ name: CI/CD LyricsScraperNET - on: push: pull_request: - jobs: lyrics_scraper_net-cicd: - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v4 - - uses: actions/setup-dotnet@v3 - with: - dotnet-version: "3.1.x" - - uses: actions/setup-dotnet@v3 - with: - dotnet-version: "5.0.x" - - uses: actions/setup-dotnet@v3 - with: - dotnet-version: "6.0.x" - - uses: actions/setup-dotnet@v3 - with: - dotnet-version: "7.0.x" - - uses: actions/setup-dotnet@v3 + uses: actions/checkout@v7 + + - name: Setup .NET SDKs + uses: actions/setup-dotnet@v6 with: - dotnet-version: "8.0.x" + dotnet-version: | + 8.0.x + 9.0.x + 10.0.x + - name: Set RELEASE_VERSION run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV + - name: Restore Packages run: dotnet restore + - name: Lint run: dotnet format --verify-no-changes --exclude *\xunit*\* + - name: Build run: dotnet build --configuration Release --verbosity minimal + - name: Test (Unit) with Coverage run: | dotnet test Tests/LyricsScraperNET.UnitTest \ @@ -40,47 +36,46 @@ jobs: --collect:"XPlat Code Coverage" \ --results-directory ./TestResults \ --settings coverlet.runsettings + - name: Upload Coverage to Codecov - uses: codecov/codecov-action@v5 + uses: codecov/codecov-action@v7 with: files: ./TestResults/**/*.cobertura.xml token: ${{ secrets.CODECOV_TOKEN }} fail_ci_if_error: true + # Temporarily switched off. There are errors during CI. # - name: Test (Integration) # run: dotnet test Tests/LyricsScraperNET.IntegrationTest + - name: Publish to NuGET run: ./publish.sh if: startsWith( github.ref, 'refs/tags/') env: NUGET_TOKEN: ${{ secrets.NUGET_TOKEN }} + - name: Publish to GH Release - uses: actions/create-release@v1 + uses: softprops/action-gh-release@v3 if: startsWith( github.ref, 'refs/tags/') - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: tag_name: ${{ github.ref }} - release_name: LyricsScraperNET ${{ github.ref }} + name: LyricsScraperNET ${{ github.ref_name }} body: | TODO draft: true prerelease: false + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Upload to GH Release if: startsWith( github.ref, 'refs/tags/') run: | - zip -j LyricsScraperNET-netstandard2.0.zip LyricsScraperNET/bin/Release/netstandard2.0/* - zip -j LyricsScraperNET-netstandard2.1.zip LyricsScraperNET/bin/Release/netstandard2.1/* - zip -j LyricsScraperNET-net5.0.zip LyricsScraperNET/bin/Release/net5.0/* - zip -j LyricsScraperNET-net6.0.zip LyricsScraperNET/bin/Release/net6.0/* - zip -j LyricsScraperNET-net7.0.zip LyricsScraperNET/bin/Release/net7.0/* zip -j LyricsScraperNET-net8.0.zip LyricsScraperNET/bin/Release/net8.0/* + zip -j LyricsScraperNET-net9.0.zip LyricsScraperNET/bin/Release/net9.0/* + zip -j LyricsScraperNET-net10.0.zip LyricsScraperNET/bin/Release/net10.0/* gh release upload "$RELEASE_VERSION" \ - "LyricsScraperNET-netstandard2.0.zip" \ - "LyricsScraperNET-netstandard2.1.zip" \ - "LyricsScraperNET-net5.0.zip" \ - "LyricsScraperNET-net6.0.zip" \ - "LyricsScraperNET-net7.0.zip" \ "LyricsScraperNET-net8.0.zip" \ + "LyricsScraperNET-net9.0.zip" \ + "LyricsScraperNET-net10.0.zip" env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file From de6676eb5be35ec5c3523be74a3c12ae8b901c36 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=92=D0=B8=D1=82=D0=B0=D0=BB=D0=B8=D0=B9=20=D0=9A=D0=B2?= =?UTF-8?q?=D0=B0=D1=88=D0=B8=D0=BD?= Date: Sun, 30 Aug 2026 20:47:33 +0200 Subject: [PATCH 04/11] #52 Fixed Readme SDKs --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 25f309d..c82162f 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ ## 🌟 Features -- **Multi-framework support**: Compatible with `.NET Standard 2.x`, `.NET 5`, `.NET 6`, `.NET 7`, `.NET 8`. +- **Multi-framework support**: Compatible with `.NET 8`, `.NET 9`, `.NET 10`. - **Integrated logging**: Effortless debugging and tracking. - **Modular architecture**: Highly testable and customizable. - **Flexible configuration**: Multiple ways to configure the library. From 7936b0b18521188c7f30ac2ec75e7aa53d0502aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=92=D0=B8=D1=82=D0=B0=D0=BB=D0=B8=D0=B9=20=D0=9A=D0=B2?= =?UTF-8?q?=D0=B0=D1=88=D0=B8=D0=BD?= Date: Sun, 30 Aug 2026 20:51:10 +0200 Subject: [PATCH 05/11] #52 Fixed Lint errors --- .../Providers/Abstract/ExternalProviderBase.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/LyricsScraperNET/Providers/Abstract/ExternalProviderBase.cs b/LyricsScraperNET/Providers/Abstract/ExternalProviderBase.cs index be670c0..a492651 100644 --- a/LyricsScraperNET/Providers/Abstract/ExternalProviderBase.cs +++ b/LyricsScraperNET/Providers/Abstract/ExternalProviderBase.cs @@ -29,9 +29,9 @@ public virtual SearchResult SearchLyric(SearchRequest searchRequest, Cancellatio return searchRequest switch { - ArtistAndSongSearchRequest artistAndSongSearchRequest + ArtistAndSongSearchRequest artistAndSongSearchRequest => SearchLyric(artistAndSongSearchRequest.Artist, artistAndSongSearchRequest.Song, cancellationToken), - UriSearchRequest uriSearchRequest + UriSearchRequest uriSearchRequest => SearchLyric(uriSearchRequest.Uri, cancellationToken), _ => new SearchResult(), }; @@ -54,9 +54,9 @@ public virtual async Task SearchLyricAsync(SearchRequest searchReq return searchRequest switch { - ArtistAndSongSearchRequest artistAndSongSearchRequest + ArtistAndSongSearchRequest artistAndSongSearchRequest => await SearchLyricAsync(artistAndSongSearchRequest.Artist, artistAndSongSearchRequest.Song, cancellationToken), - UriSearchRequest uriSearchRequest + UriSearchRequest uriSearchRequest => await SearchLyricAsync(uriSearchRequest.Uri, cancellationToken), _ => new SearchResult(), }; From 45104895be17066fe0a4122f4c12c482d62d6d79 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=92=D0=B8=D1=82=D0=B0=D0=BB=D0=B8=D0=B9=20=D0=9A=D0=B2?= =?UTF-8?q?=D0=B0=D1=88=D0=B8=D0=BD?= Date: Sun, 30 Aug 2026 20:55:56 +0200 Subject: [PATCH 06/11] #52 Try to fix cicd --- Tests/LyricsScraperNET.UnitTest/LyricsScraperNET.UnitTest.csproj | 1 + 1 file changed, 1 insertion(+) diff --git a/Tests/LyricsScraperNET.UnitTest/LyricsScraperNET.UnitTest.csproj b/Tests/LyricsScraperNET.UnitTest/LyricsScraperNET.UnitTest.csproj index 5aed30e..a171c1a 100644 --- a/Tests/LyricsScraperNET.UnitTest/LyricsScraperNET.UnitTest.csproj +++ b/Tests/LyricsScraperNET.UnitTest/LyricsScraperNET.UnitTest.csproj @@ -8,6 +8,7 @@ net10.0;net9.0;net8.0 latest false + true From 2f9d06d1f53c2e21efee07fba32c05444cec4166 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=92=D0=B8=D1=82=D0=B0=D0=BB=D0=B8=D0=B9=20=D0=9A=D0=B2?= =?UTF-8?q?=D0=B0=D1=88=D0=B8=D0=BD?= Date: Sun, 30 Aug 2026 21:01:38 +0200 Subject: [PATCH 07/11] #52 Removed .net10 unit tests temp --- .../LyricsScraperNET.UnitTest/LyricsScraperNET.UnitTest.csproj | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Tests/LyricsScraperNET.UnitTest/LyricsScraperNET.UnitTest.csproj b/Tests/LyricsScraperNET.UnitTest/LyricsScraperNET.UnitTest.csproj index a171c1a..d0d4803 100644 --- a/Tests/LyricsScraperNET.UnitTest/LyricsScraperNET.UnitTest.csproj +++ b/Tests/LyricsScraperNET.UnitTest/LyricsScraperNET.UnitTest.csproj @@ -5,10 +5,9 @@ - net10.0;net9.0;net8.0 + net9.0;net8.0 latest false - true From e4c65b1d7833ce543426c925851a5d8870e07998 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=92=D0=B8=D1=82=D0=B0=D0=BB=D0=B8=D0=B9=20=D0=9A=D0=B2?= =?UTF-8?q?=D0=B0=D1=88=D0=B8=D0=BD?= Date: Sun, 30 Aug 2026 21:05:31 +0200 Subject: [PATCH 08/11] #52 Try to fix cicd and coverlet --- .../LyricsScraperNET.IntegrationTest.csproj | 2 +- .../LyricsScraperNET.UnitTest.csproj | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/Tests/LyricsScraperNET.IntegrationTest/LyricsScraperNET.IntegrationTest.csproj b/Tests/LyricsScraperNET.IntegrationTest/LyricsScraperNET.IntegrationTest.csproj index d77098f..bc964a1 100644 --- a/Tests/LyricsScraperNET.IntegrationTest/LyricsScraperNET.IntegrationTest.csproj +++ b/Tests/LyricsScraperNET.IntegrationTest/LyricsScraperNET.IntegrationTest.csproj @@ -13,7 +13,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/Tests/LyricsScraperNET.UnitTest/LyricsScraperNET.UnitTest.csproj b/Tests/LyricsScraperNET.UnitTest/LyricsScraperNET.UnitTest.csproj index d0d4803..7690f37 100644 --- a/Tests/LyricsScraperNET.UnitTest/LyricsScraperNET.UnitTest.csproj +++ b/Tests/LyricsScraperNET.UnitTest/LyricsScraperNET.UnitTest.csproj @@ -5,9 +5,10 @@ - net9.0;net8.0 + net10.0;net9.0;net8.0 latest false + false @@ -16,7 +17,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive From 4d3d271a0fbed78967210ae001d04ad148cdeee9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=92=D0=B8=D1=82=D0=B0=D0=BB=D0=B8=D0=B9=20=D0=9A=D0=B2?= =?UTF-8?q?=D0=B0=D1=88=D0=B8=D0=BD?= Date: Sun, 30 Aug 2026 21:10:08 +0200 Subject: [PATCH 09/11] #52 Try to fix tests running --- global.json | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 global.json diff --git a/global.json b/global.json new file mode 100644 index 0000000..8f73781 --- /dev/null +++ b/global.json @@ -0,0 +1,5 @@ +{ + "test": { + "runner": "Microsoft.Testing.Platform" + } +} \ No newline at end of file From 939beb65713180ea1c57decdd43ed93baf0293bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=92=D0=B8=D1=82=D0=B0=D0=BB=D0=B8=D0=B9=20=D0=9A=D0=B2?= =?UTF-8?q?=D0=B0=D1=88=D0=B8=D0=BD?= Date: Sun, 30 Aug 2026 21:17:15 +0200 Subject: [PATCH 10/11] #52 Fixing code coverage --- .github/workflows/cicd.yaml | 4 ++-- Scripts/run_tests_and_coverage.sh | 5 ++--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/.github/workflows/cicd.yaml b/.github/workflows/cicd.yaml index 6f6253a..4f3009c 100644 --- a/.github/workflows/cicd.yaml +++ b/.github/workflows/cicd.yaml @@ -33,9 +33,9 @@ jobs: run: | dotnet test Tests/LyricsScraperNET.UnitTest \ --configuration Release \ - --collect:"XPlat Code Coverage" \ --results-directory ./TestResults \ - --settings coverlet.runsettings + -- \ + --coverage --coverage-output-format cobertura - name: Upload Coverage to Codecov uses: codecov/codecov-action@v7 diff --git a/Scripts/run_tests_and_coverage.sh b/Scripts/run_tests_and_coverage.sh index f2af5bc..04b2e38 100644 --- a/Scripts/run_tests_and_coverage.sh +++ b/Scripts/run_tests_and_coverage.sh @@ -1,11 +1,10 @@ #!/bin/bash - # Change to the root directory of the project cd "$(dirname "$0")/.." # Run the unit tests with coverlet and output the results in Cobertura format dotnet test Tests/LyricsScraperNET.UnitTest \ --configuration Release \ - --collect:"XPlat Code Coverage" \ --results-directory ./TestResults \ - --settings coverlet.runsettings + -- \ + --coverage --coverage-output-format cobertura \ No newline at end of file From fcb01560919650361134bba31c04d0fe5dac636e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=92=D0=B8=D1=82=D0=B0=D0=BB=D0=B8=D0=B9=20=D0=9A=D0=B2?= =?UTF-8?q?=D0=B0=D1=88=D0=B8=D0=BD?= Date: Sun, 30 Aug 2026 22:37:29 +0200 Subject: [PATCH 11/11] #52 Removed coverlet --- .../LyricsScraperNET.IntegrationTest.csproj | 5 +---- .../LyricsScraperClientTests.cs | 8 ++++++-- .../LyricsScraperNET.UnitTest.csproj | 7 ++----- coverlet.runsettings | 15 --------------- 4 files changed, 9 insertions(+), 26 deletions(-) delete mode 100644 coverlet.runsettings diff --git a/Tests/LyricsScraperNET.IntegrationTest/LyricsScraperNET.IntegrationTest.csproj b/Tests/LyricsScraperNET.IntegrationTest/LyricsScraperNET.IntegrationTest.csproj index bc964a1..81505d0 100644 --- a/Tests/LyricsScraperNET.IntegrationTest/LyricsScraperNET.IntegrationTest.csproj +++ b/Tests/LyricsScraperNET.IntegrationTest/LyricsScraperNET.IntegrationTest.csproj @@ -13,10 +13,7 @@ - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/Tests/LyricsScraperNET.UnitTest/LyricsScraperClientTests.cs b/Tests/LyricsScraperNET.UnitTest/LyricsScraperClientTests.cs index ca545ad..e2df6ac 100644 --- a/Tests/LyricsScraperNET.UnitTest/LyricsScraperClientTests.cs +++ b/Tests/LyricsScraperNET.UnitTest/LyricsScraperClientTests.cs @@ -449,7 +449,7 @@ public async Task SearchLyricAsync_WithUseParallelSearchEnabled_ShouldReturnFirs { await Task.Delay(1000, ct); // Simulate a fast execution. return fastResult; - }); ; + }); A.CallTo(() => fastProvider.IsEnabled).Returns(true); // Create slow providers that simulate delayed response. @@ -567,7 +567,11 @@ public async Task SearchLyricAsync_WithUseParallelSearchEnabled_ShouldUseOnlyChi var fastResult = new SearchResult("Fast result", ExternalProviderType.None); var fastProvider = A.Fake(); A.CallTo(() => fastProvider.SearchLyricAsync(A._, A._)) - .Returns(fastResult); + .ReturnsLazily(async (SearchRequest r, CancellationToken ct) => + { + await Task.Delay(1000, ct); // Simulate a fast execution. + return fastResult; + }); A.CallTo(() => fastProvider.IsEnabled).Returns(true); // Create slow providers that simulate delayed response but are not used because fast provider wins. diff --git a/Tests/LyricsScraperNET.UnitTest/LyricsScraperNET.UnitTest.csproj b/Tests/LyricsScraperNET.UnitTest/LyricsScraperNET.UnitTest.csproj index 7690f37..6399192 100644 --- a/Tests/LyricsScraperNET.UnitTest/LyricsScraperNET.UnitTest.csproj +++ b/Tests/LyricsScraperNET.UnitTest/LyricsScraperNET.UnitTest.csproj @@ -5,7 +5,7 @@ - net10.0;net9.0;net8.0 + net10.0 latest false false @@ -17,10 +17,7 @@ - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/coverlet.runsettings b/coverlet.runsettings deleted file mode 100644 index a2cd6d0..0000000 --- a/coverlet.runsettings +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - [LyricsScraperNET.TestShared*]*, - [LyricsScraperNET.Client*]* - - cobertura - - - - -