diff --git a/.github/workflows/cicd.yaml b/.github/workflows/cicd.yaml index b7da333..4f3009c 100644 --- a/.github/workflows/cicd.yaml +++ b/.github/workflows/cicd.yaml @@ -1,86 +1,81 @@ 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 \ --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@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 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.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 0b7bad4..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; @@ -13,8 +14,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) @@ -28,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/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/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/Abstract/ExternalProviderBase.cs b/LyricsScraperNET/Providers/Abstract/ExternalProviderBase.cs index 6534d30..a492651 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/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/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/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/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/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..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. @@ -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/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 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..81505d0 100644 --- a/Tests/LyricsScraperNET.IntegrationTest/LyricsScraperNET.IntegrationTest.csproj +++ b/Tests/LyricsScraperNET.IntegrationTest/LyricsScraperNET.IntegrationTest.csproj @@ -5,25 +5,20 @@ - 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 + @@ -107,6 +102,12 @@ PreserveNewest + + PreserveNewest + + + PreserveNewest + 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/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.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/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 a606de4..e2df6ac 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; @@ -443,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. @@ -476,7 +482,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 +543,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); @@ -561,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. @@ -638,7 +648,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 +699,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 +716,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 +737,7 @@ private ILyricsScraperClient GetLyricsScraperClientWithMockedProvider() return client; } - private IExternalProvider GetExternalProviderMock(ExternalProviderType externalProviderType) + private static IExternalProvider GetExternalProviderMock(ExternalProviderType externalProviderType) { var externalProviderMock = A.Fake(); @@ -750,7 +760,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..6399192 100644 --- a/Tests/LyricsScraperNET.UnitTest/LyricsScraperNET.UnitTest.csproj +++ b/Tests/LyricsScraperNET.UnitTest/LyricsScraperNET.UnitTest.csproj @@ -5,36 +5,24 @@ - net8.0;net7.0;net6.0;net5.0;netcoreapp3.1 - 9.0 + net10.0 + latest false + 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 + @@ -133,6 +121,27 @@ PreserveNewest + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + 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/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/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 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 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 - - - - - 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