diff --git a/LyricsScraperNET.Client/Program.cs b/LyricsScraperNET.Client/Program.cs index 93b9575..c3eb549 100644 --- a/LyricsScraperNET.Client/Program.cs +++ b/LyricsScraperNET.Client/Program.cs @@ -12,6 +12,7 @@ using LyricsScraperNET.Models.Responses; using System.Threading.Tasks; using System; +using Microsoft.Extensions.Logging; class Program { @@ -28,11 +29,23 @@ static async Task Main() //var result = ExampleWithCertainProvider(artistToSearch, songToSearch); //// Checking if something was found. - //// If not, search errors can be found in the logs. + //// May not be found in two cases: + //// 1) search errors can be found in the logs or in response fields like ResponseStatusCode and ResponseMessage. + //// 2) The requested song contains only the instrumental, no lyrics. In this case the flag will be true. if (result.IsEmpty()) { - Console.ForegroundColor = ConsoleColor.Red; - Console.WriteLine($"Can't find lyrics for: {artistToSearch} - {songToSearch}"); + if (result.Instrumental) + { + Console.ForegroundColor = ConsoleColor.Gray; + Console.WriteLine("This song is instrumental.\r\nIt does not contain any lyrics"); + } + else + { + Console.ForegroundColor = ConsoleColor.Red; + Console.WriteLine($"Can't find lyrics for: [ {artistToSearch} - {songToSearch} ]. " + + $"Status code: [ {result.ResponseStatusCode} ]. " + + $"Response message: [ {result.ResponseMessage} ]."); + } Console.ResetColor(); Console.ReadLine(); @@ -41,13 +54,13 @@ static async Task Main() //// Output result to console Console.ForegroundColor = ConsoleColor.Yellow; - Console.WriteLine($"{artistToSearch} - {songToSearch}\r\n"); + Console.WriteLine($"[ {artistToSearch} - {songToSearch} ]\r\n"); Console.ResetColor(); Console.WriteLine(result.LyricText); Console.ForegroundColor = ConsoleColor.Magenta; - Console.WriteLine($"\r\nThis text was found by {result.ExternalProviderType}\r\n"); + Console.WriteLine($"\r\nThis lyric was found by [ {result.ExternalProviderType} ]\r\n"); Console.ResetColor(); Console.ReadLine(); @@ -105,6 +118,16 @@ ILyricsScraperClient lyricsScraperClient .WithSongLyrics() .WithLyricFind(); + // To enable library logging, the LoggerFactory must be configured and passed to the client. + var loggerFactory = LoggerFactory.Create(builder => + { + builder.AddFilter("Microsoft", LogLevel.Warning) + .AddFilter("System", LogLevel.Warning) + .AddFilter("LyricsScraperNET", LogLevel.Trace) + .AddConsole(); + }); + lyricsScraperClient.WithLogger(loggerFactory); + //// Another way to configure: //// 1. First create instance of LyricScraperClient. // ILyricsScraperClient lyricsScraperClient = new LyricsScraperClient(); diff --git a/LyricsScraperNET/Extensions/SearchResultExtensions.cs b/LyricsScraperNET/Extensions/SearchResultExtensions.cs index b82bb9e..97b7ce5 100644 --- a/LyricsScraperNET/Extensions/SearchResultExtensions.cs +++ b/LyricsScraperNET/Extensions/SearchResultExtensions.cs @@ -29,5 +29,11 @@ internal static SearchResult AppendResponseMessage(this SearchResult searchResul : responseMessage; return searchResult; } + + internal static SearchResult AddInstrumental(this SearchResult searchResult, bool instrumental) + { + searchResult.Instrumental = instrumental; + return searchResult; + } } } diff --git a/LyricsScraperNET/ILyricsScraperClient.cs b/LyricsScraperNET/ILyricsScraperClient.cs index e493176..a12e6b6 100644 --- a/LyricsScraperNET/ILyricsScraperClient.cs +++ b/LyricsScraperNET/ILyricsScraperClient.cs @@ -2,6 +2,7 @@ using LyricsScraperNET.Models.Responses; using LyricsScraperNET.Providers.Abstract; using LyricsScraperNET.Providers.Models; +using Microsoft.Extensions.Logging; using System.Threading.Tasks; namespace LyricsScraperNET @@ -49,5 +50,11 @@ public interface ILyricsScraperClient /// Calling the lyrics search method will return an empty result. /// void Disable(); + + /// + /// Creates a new ILogger instance from . + /// Can be useful for error analysis if the lyric text is not found. + /// + void WithLogger(ILoggerFactory loggerFactory); } } diff --git a/LyricsScraperNET/LyricsScraperClient.cs b/LyricsScraperNET/LyricsScraperClient.cs index dc5e33f..7437b65 100644 --- a/LyricsScraperNET/LyricsScraperClient.cs +++ b/LyricsScraperNET/LyricsScraperClient.cs @@ -16,8 +16,8 @@ namespace LyricsScraperNET { public sealed class LyricsScraperClient : ILyricsScraperClient { - - private readonly ILogger _logger; + private ILoggerFactory _loggerFactory; + private ILogger _logger; private List _externalProviders; private readonly ILyricScraperClientConfig _lyricScraperClientConfig; @@ -200,7 +200,11 @@ public void AddProvider(IExternalProvider provider) if (IsEmptyProvidersList()) _externalProviders = new List(); if (!_externalProviders.Contains(provider)) + { + if (_loggerFactory != null) + provider.WithLogger(_loggerFactory); _externalProviders.Add(provider); + } else _logger?.LogWarning($"External provider {provider} already added"); } @@ -219,9 +223,7 @@ public void Enable() return; foreach (var provider in _externalProviders) - { provider.Enable(); - } } public void Disable() @@ -230,9 +232,19 @@ public void Disable() return; foreach (var provider in _externalProviders) - { provider.Disable(); - } + } + + public void WithLogger(ILoggerFactory loggerFactory) + { + _loggerFactory = loggerFactory; + _logger = loggerFactory.CreateLogger(); + + if (IsEmptyProvidersList()) + return; + + foreach (var provider in _externalProviders) + provider.WithLogger(loggerFactory); } private bool IsEmptyProvidersList() => _externalProviders == null || !_externalProviders.Any(); diff --git a/LyricsScraperNET/Models/Responses/SearchResult.cs b/LyricsScraperNET/Models/Responses/SearchResult.cs index b27cf73..7e09bfe 100644 --- a/LyricsScraperNET/Models/Responses/SearchResult.cs +++ b/LyricsScraperNET/Models/Responses/SearchResult.cs @@ -47,6 +47,11 @@ internal SearchResult(string lyricText, ExternalProviderType externalProviderTyp /// public string ResponseMessage { get; internal set; } = string.Empty; + /// + /// The flag indicates that the search results are for music only, without text. + /// + public bool Instrumental { get; internal set; } = false; + /// /// Returns true if the field is empty. /// diff --git a/LyricsScraperNET/Providers/AZLyrics/AZLyricsProvider.cs b/LyricsScraperNET/Providers/AZLyrics/AZLyricsProvider.cs index c2a349b..3264fb2 100644 --- a/LyricsScraperNET/Providers/AZLyrics/AZLyricsProvider.cs +++ b/LyricsScraperNET/Providers/AZLyrics/AZLyricsProvider.cs @@ -11,7 +11,7 @@ namespace LyricsScraperNET.Providers.AZLyrics { public sealed class AZLyricsProvider : ExternalProviderBase { - private readonly ILogger _logger; + private ILogger _logger; private readonly IExternalUriConverter _uriConverter; private const string _lyricStart = ""; @@ -96,6 +96,11 @@ protected override async Task SearchLyricAsync(Uri uri) #endregion + public override void WithLogger(ILoggerFactory loggerFactory) + { + _logger = loggerFactory.CreateLogger(); + } + private SearchResult PostProcessLyric(Uri uri, string text) { if (string.IsNullOrEmpty(text)) diff --git a/LyricsScraperNET/Providers/Abstract/ExternalProviderBase.cs b/LyricsScraperNET/Providers/Abstract/ExternalProviderBase.cs index ec0ef29..acfcdba 100644 --- a/LyricsScraperNET/Providers/Abstract/ExternalProviderBase.cs +++ b/LyricsScraperNET/Providers/Abstract/ExternalProviderBase.cs @@ -1,6 +1,7 @@ using LyricsScraperNET.Models.Requests; using LyricsScraperNET.Models.Responses; using LyricsScraperNET.Network.Abstract; +using Microsoft.Extensions.Logging; using System; using System.Threading.Tasks; @@ -95,5 +96,8 @@ public void Disable() if (Options != null) Options.Enabled = false; } + + public virtual void WithLogger(ILoggerFactory loggerFactory) + => throw new NotImplementedException(); } } diff --git a/LyricsScraperNET/Providers/Abstract/IExternalProvider.cs b/LyricsScraperNET/Providers/Abstract/IExternalProvider.cs index d91639f..a581ef7 100644 --- a/LyricsScraperNET/Providers/Abstract/IExternalProvider.cs +++ b/LyricsScraperNET/Providers/Abstract/IExternalProvider.cs @@ -1,6 +1,7 @@ using LyricsScraperNET.Models.Requests; using LyricsScraperNET.Models.Responses; using LyricsScraperNET.Network.Abstract; +using Microsoft.Extensions.Logging; using System.Threading.Tasks; namespace LyricsScraperNET.Providers.Abstract @@ -27,5 +28,7 @@ public interface IExternalProvider void Enable(); void Disable(); + + void WithLogger(ILoggerFactory loggerFactory); } } diff --git a/LyricsScraperNET/Providers/Genius/GeniusProvider.cs b/LyricsScraperNET/Providers/Genius/GeniusProvider.cs index 515404c..5720ea5 100644 --- a/LyricsScraperNET/Providers/Genius/GeniusProvider.cs +++ b/LyricsScraperNET/Providers/Genius/GeniusProvider.cs @@ -17,7 +17,7 @@ namespace LyricsScraperNET.Providers.Genius { public sealed class GeniusProvider : ExternalProviderBase { - private readonly ILogger _logger; + private ILogger _logger; private readonly IExternalUriConverter _uriConverter; // Format: "artist song". Example: "Parkway Drive Carrion". @@ -26,6 +26,10 @@ public sealed class GeniusProvider : ExternalProviderBase private const string _referentFragmentNodesXPath = "//a[contains(@class, 'ReferentFragmentVariantdesktop') or contains(@class, 'ReferentFragmentdesktop')]"; private const string _lyricsContainerNodesXPath = "//div[@data-lyrics-container]"; + // In case of instrumental song without a lyric. + private const string _lyricsPlaceholderNodesXPath = "//div[contains(@class, 'LyricsPlaceholder')]"; + private const string _instrumentalLyricText = "This song is an instrumental"; + #region Constructors public GeniusProvider() @@ -71,7 +75,10 @@ protected override SearchResult SearchLyric(Uri uri) { var htmlPageBody = WebClient.Load(uri); - return new SearchResult(GetParsedLyricFromHtmlPageBody(htmlPageBody), Models.ExternalProviderType.Genius); + var lyricResult = GetParsedLyricFromHtmlPageBody(htmlPageBody, out var instrumental); + + return new SearchResult(lyricResult, Models.ExternalProviderType.Genius) + .AddInstrumental(instrumental); } protected override SearchResult SearchLyric(string artist, string song) @@ -94,7 +101,7 @@ protected override SearchResult SearchLyric(string artist, string song) return !string.IsNullOrWhiteSpace(lyricUrl) ? SearchLyric(new Uri(lyricUrl)) - : new SearchResult(); + : new SearchResult(Models.ExternalProviderType.Genius); } #endregion @@ -105,7 +112,10 @@ protected override async Task SearchLyricAsync(Uri uri) { var htmlPageBody = await WebClient.LoadAsync(uri); - return new SearchResult(GetParsedLyricFromHtmlPageBody(htmlPageBody), Models.ExternalProviderType.Genius); + var lyricResult = GetParsedLyricFromHtmlPageBody(htmlPageBody, out var instrumental); + + return new SearchResult(lyricResult, Models.ExternalProviderType.Genius) + .AddInstrumental(instrumental); } protected override async Task SearchLyricAsync(string artist, string song) @@ -128,11 +138,16 @@ protected override async Task SearchLyricAsync(string artist, stri return !string.IsNullOrWhiteSpace(lyricUrl) ? await SearchLyricAsync(new Uri(lyricUrl)) - : new SearchResult(); + : new SearchResult(Models.ExternalProviderType.Genius); } #endregion + public override void WithLogger(ILoggerFactory loggerFactory) + { + _logger = loggerFactory.CreateLogger(); + } + private string GetLyricUrlWithoutApiKey(string artist, string song) { var htmlPageBody = WebClient.Load(_uriConverter.GetLyricUri(artist, song)); @@ -160,8 +175,10 @@ private string GetLyricUrlWithoutApiKey(string artist, string song) return string.Empty; } - private string GetParsedLyricFromHtmlPageBody(string htmlPageBody) + private string GetParsedLyricFromHtmlPageBody(string htmlPageBody, out bool instrumental) { + instrumental = false; + var htmlDocument = new HtmlDocument(); htmlDocument.LoadHtml(htmlPageBody.Replace("https://ajax.googleapis.com/ajax/libs/jquery/2.1.4/jquery.min.js", "")); @@ -175,8 +192,20 @@ private string GetParsedLyricFromHtmlPageBody(string htmlPageBody) spanNode.Remove(); var lyricNodes = htmlDocument.DocumentNode.SelectNodes(_lyricsContainerNodesXPath); + if (lyricNodes == null) + { + // lyricNodes could be null in case of instrumental. + var instrumentalNodes = htmlDocument.DocumentNode.SelectNodes(_lyricsPlaceholderNodesXPath); + if (instrumentalNodes != null + && instrumentalNodes.Any(node => node.InnerHtml.Contains(_instrumentalLyricText))) + { + instrumental = true; + return string.Empty; + } + _logger?.LogWarning($"Genius. Can't parse lyric from the page."); + } - return Parser.Parse(string.Join("", lyricNodes.Select(Node => Node.InnerHtml))); + return Parser.Parse(string.Join("", lyricNodes.Select(node => node.InnerHtml))); } private string GetLyricUrlFromSearchResponse(SearchResponse searchResponse, string artist, string song) diff --git a/LyricsScraperNET/Providers/LyricFind/LyricFindProvider.cs b/LyricsScraperNET/Providers/LyricFind/LyricFindProvider.cs index 549c064..a28b009 100644 --- a/LyricsScraperNET/Providers/LyricFind/LyricFindProvider.cs +++ b/LyricsScraperNET/Providers/LyricFind/LyricFindProvider.cs @@ -11,7 +11,7 @@ namespace LyricsScraperNET.Providers.LyricFind { public sealed class LyricFindProvider : ExternalProviderBase { - private readonly ILogger _logger; + private ILogger _logger; private readonly IExternalUriConverter _uriConverter; private const string _lyricStart = "\"lyrics\""; @@ -95,6 +95,11 @@ protected override async Task SearchLyricAsync(Uri uri) #endregion + public override void WithLogger(ILoggerFactory loggerFactory) + { + _logger = loggerFactory.CreateLogger(); + } + private SearchResult PostProcessLyric(Uri uri, string text) { if (string.IsNullOrEmpty(text)) diff --git a/LyricsScraperNET/Providers/Musixmatch/MusixmatchProvider.cs b/LyricsScraperNET/Providers/Musixmatch/MusixmatchProvider.cs index 01747fa..3be6855 100644 --- a/LyricsScraperNET/Providers/Musixmatch/MusixmatchProvider.cs +++ b/LyricsScraperNET/Providers/Musixmatch/MusixmatchProvider.cs @@ -1,4 +1,5 @@ -using LyricsScraperNET.Helpers; +using LyricsScraperNET.Extensions; +using LyricsScraperNET.Helpers; using LyricsScraperNET.Models.Responses; using LyricsScraperNET.Providers.Abstract; using Microsoft.Extensions.Caching.Memory; @@ -17,7 +18,7 @@ namespace LyricsScraperNET.Providers.Musixmatch { public sealed class MusixmatchProvider : ExternalProviderBase { - private readonly ILogger _logger; + private ILogger _logger; // Musixmatch Token memory cache private static readonly IMemoryCache _memoryCache; @@ -80,7 +81,7 @@ public MusixmatchProvider(IOptionsSnapshot options) // TODO: search by uri from the site. Example: https://www.musixmatch.com/lyrics/Parkway-Drive/Idols-and-Anchors protected override SearchResult SearchLyric(Uri uri) { - return new SearchResult(); + return new SearchResult(Models.ExternalProviderType.Musixmatch); } protected override SearchResult SearchLyric(string artist, string song) @@ -97,14 +98,19 @@ protected override SearchResult SearchLyric(string artist, string song) if (trackId != null) { Lyrics lyrics = client.GetTrackLyrics(trackId.Value); - return lyrics.Instrumental != 1 - ? new SearchResult(lyrics.LyricsBody, Models.ExternalProviderType.Musixmatch) - : new SearchResult(); // lyrics.LyricsBody is null when the track is instrumental + + // lyrics.LyricsBody is null when the track is instrumental + if (lyrics.Instrumental != 1) + return new SearchResult(lyrics.LyricsBody, Models.ExternalProviderType.Musixmatch); + + // Instrumental music without lyric + return new SearchResult(Models.ExternalProviderType.Musixmatch) + .AddInstrumental(true); } else { _logger?.LogWarning($"Musixmatch. Can't find any information about artist {artist} and song {song}"); - return new SearchResult(); + return new SearchResult(Models.ExternalProviderType.Musixmatch); } } catch (MusixmatchRequestException requestException) when (requestException.StatusCode == StatusCode.AuthFailed) @@ -113,7 +119,7 @@ protected override SearchResult SearchLyric(string artist, string song) regenerateToken = true; } } - return new SearchResult(); + return new SearchResult(Models.ExternalProviderType.Musixmatch); } #endregion @@ -123,7 +129,7 @@ protected override SearchResult SearchLyric(string artist, string song) // TODO: search by uri from the site. Example: https://www.musixmatch.com/lyrics/Parkway-Drive/Idols-and-Anchors protected override Task SearchLyricAsync(Uri uri) { - return Task.FromResult(new SearchResult()); + return Task.FromResult(new SearchResult(Models.ExternalProviderType.Musixmatch)); } protected override async Task SearchLyricAsync(string artist, string song) @@ -141,14 +147,19 @@ protected override async Task SearchLyricAsync(string artist, stri if (trackId != null) { Lyrics lyrics = await client.GetTrackLyricsAsync(trackId.Value); - return lyrics.Instrumental != 1 - ? new SearchResult(lyrics.LyricsBody, Models.ExternalProviderType.Musixmatch) - : new SearchResult(); // lyrics.LyricsBody is null when the track is instrumental + + // lyrics.LyricsBody is null when the track is instrumental + if (lyrics.Instrumental != 1) + return new SearchResult(lyrics.LyricsBody, Models.ExternalProviderType.Musixmatch); + + // Instrumental music without lyric + return new SearchResult(Models.ExternalProviderType.Musixmatch) + .AddInstrumental(true); } else { _logger?.LogWarning($"Musixmatch. Can't find any information about artist {artist} and song {song}"); - return new SearchResult(); + return new SearchResult(Models.ExternalProviderType.Musixmatch); } } catch (MusixmatchRequestException requestException) when (requestException.StatusCode == StatusCode.AuthFailed) @@ -157,11 +168,16 @@ protected override async Task SearchLyricAsync(string artist, stri regenerateToken = true; } } - return new SearchResult(); + return new SearchResult(Models.ExternalProviderType.Musixmatch); } #endregion + public override void WithLogger(ILoggerFactory loggerFactory) + { + _logger = loggerFactory.CreateLogger(); + } + private MusixmatchClient GetMusixmatchClient(bool regenerateToken = false) { // TODO: uncomment after the fix of https://github.com/Eimaen/MusixmatchClientLib/issues/21 diff --git a/LyricsScraperNET/Providers/SongLyrics/SongLyricsProvider.cs b/LyricsScraperNET/Providers/SongLyrics/SongLyricsProvider.cs index f517b5a..c6cd306 100644 --- a/LyricsScraperNET/Providers/SongLyrics/SongLyricsProvider.cs +++ b/LyricsScraperNET/Providers/SongLyrics/SongLyricsProvider.cs @@ -13,7 +13,7 @@ namespace LyricsScraperNET.Providers.SongLyrics { public sealed class SongLyricsProvider : ExternalProviderBase { - private readonly ILogger _logger; + private ILogger _logger; private readonly IExternalUriConverter _uriConverter; @@ -59,7 +59,6 @@ public SongLyricsProvider(IOptionsSnapshot options) public override IExternalProviderOptions Options { get; } - #region Sync protected override SearchResult SearchLyric(string artist, string song) @@ -80,7 +79,6 @@ protected override SearchResult SearchLyric(Uri uri) #endregion - #region Async protected override async Task SearchLyricAsync(string artist, string song) @@ -101,6 +99,10 @@ protected override async Task SearchLyricAsync(Uri uri) #endregion + public override void WithLogger(ILoggerFactory loggerFactory) + { + _logger = loggerFactory.CreateLogger(); + } private SearchResult GetParsedLyricFromHtmlPageBody(Uri uri, string htmlPageBody) {