diff --git a/LyricsScraperNET.Client/Program.cs b/LyricsScraperNET.Client/Program.cs index 0af3ebb..93b9575 100644 --- a/LyricsScraperNET.Client/Program.cs +++ b/LyricsScraperNET.Client/Program.cs @@ -4,6 +4,7 @@ using LyricsScraperNET.Providers.AZLyrics; using LyricsScraperNET.Providers.Genius; using LyricsScraperNET.Providers.SongLyrics; +using LyricsScraperNET.Providers.LyricFind; using LyricsScraperNET.Models.Requests; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; @@ -101,7 +102,8 @@ ILyricsScraperClient lyricsScraperClient .WithGenius() .WithAZLyrics() .WithMusixmatch() - .WithSongLyrics(); + .WithSongLyrics() + .WithLyricFind(); //// Another way to configure: //// 1. First create instance of LyricScraperClient. diff --git a/LyricsScraperNET.Client/appsettings.json b/LyricsScraperNET.Client/appsettings.json index 166e883..da9d6da 100644 --- a/LyricsScraperNET.Client/appsettings.json +++ b/LyricsScraperNET.Client/appsettings.json @@ -1,12 +1,12 @@ { "LyricScraperClient": { "GeniusOptions": { - "SearchPriority": 2, + "SearchPriority": 3, "Enabled": true, "ApiKey": "" }, "AZLyricsOptions": { - "SearchPriority": 3, + "SearchPriority": 4, "Enabled": true }, "MusixmatchOptions": { @@ -17,6 +17,10 @@ "SongLyricsOptions": { "SearchPriority": 0, "Enabled": true + }, + "LyricFindOptions": { + "SearchPriority": 2, + "Enabled": true } } } \ No newline at end of file diff --git a/LyricsScraperNET/Configuration/ILyricScraperClientConfig.cs b/LyricsScraperNET/Configuration/ILyricScraperClientConfig.cs index 676de10..a177006 100644 --- a/LyricsScraperNET/Configuration/ILyricScraperClientConfig.cs +++ b/LyricsScraperNET/Configuration/ILyricScraperClientConfig.cs @@ -16,5 +16,7 @@ public interface ILyricScraperClientConfig IExternalProviderOptions MusixmatchOptions { get; } IExternalProviderOptions SongLyricsOptions { get; } + + IExternalProviderOptions LyricFindOptions { get; } } } diff --git a/LyricsScraperNET/Configuration/LyricScraperClientConfig.cs b/LyricsScraperNET/Configuration/LyricScraperClientConfig.cs index ac779bf..4faad6c 100644 --- a/LyricsScraperNET/Configuration/LyricScraperClientConfig.cs +++ b/LyricsScraperNET/Configuration/LyricScraperClientConfig.cs @@ -1,6 +1,7 @@ using LyricsScraperNET.Providers.Abstract; using LyricsScraperNET.Providers.AZLyrics; using LyricsScraperNET.Providers.Genius; +using LyricsScraperNET.Providers.LyricFind; using LyricsScraperNET.Providers.Musixmatch; using LyricsScraperNET.Providers.SongLyrics; @@ -18,9 +19,12 @@ public sealed class LyricScraperClientConfig : ILyricScraperClientConfig public IExternalProviderOptions SongLyricsOptions { get; set; } = new SongLyricsOptions(); + public IExternalProviderOptions LyricFindOptions { get; set; } = new LyricFindOptions(); + public bool IsEnabled => AZLyricsOptions.Enabled || GeniusOptions.Enabled || MusixmatchOptions.Enabled - || SongLyricsOptions.Enabled; + || SongLyricsOptions.Enabled + || LyricFindOptions.Enabled; } } diff --git a/LyricsScraperNET/Configuration/ServiceCollectionExtensions.cs b/LyricsScraperNET/Configuration/ServiceCollectionExtensions.cs index 25842e1..bd33898 100644 --- a/LyricsScraperNET/Configuration/ServiceCollectionExtensions.cs +++ b/LyricsScraperNET/Configuration/ServiceCollectionExtensions.cs @@ -1,6 +1,7 @@ using LyricsScraperNET.Providers.Abstract; using LyricsScraperNET.Providers.AZLyrics; using LyricsScraperNET.Providers.Genius; +using LyricsScraperNET.Providers.LyricFind; using LyricsScraperNET.Providers.Musixmatch; using LyricsScraperNET.Providers.SongLyrics; using Microsoft.Extensions.Configuration; @@ -22,6 +23,7 @@ public static IServiceCollection AddLyricScraperClientService( services.AddGeniusClientService(lyricScraperClientConfig); services.AddMusixmatchService(lyricScraperClientConfig); services.AddSongLyricsService(lyricScraperClientConfig); + services.AddLyricFindService(lyricScraperClientConfig); services.Configure(lyricScraperClientConfig); services.AddScoped(x => x.GetRequiredService>().Value); @@ -91,5 +93,20 @@ public static IServiceCollection AddSongLyricsService( return services; } + + public static IServiceCollection AddLyricFindService( + this IServiceCollection services, + IConfiguration configuration) + { + var configurationSection = configuration.GetSection(LyricFindOptions.ConfigurationSectionName); + if (configurationSection.Exists()) + { + services.Configure(configurationSection); + + services.AddScoped(typeof(IExternalProvider), typeof(LyricFindProvider)); + } + + return services; + } } } diff --git a/LyricsScraperNET/Extensions/LyricsScraperClientExtensions.cs b/LyricsScraperNET/Extensions/LyricsScraperClientExtensions.cs index 2b9764c..c35c79d 100644 --- a/LyricsScraperNET/Extensions/LyricsScraperClientExtensions.cs +++ b/LyricsScraperNET/Extensions/LyricsScraperClientExtensions.cs @@ -1,5 +1,6 @@ using LyricsScraperNET.Providers.AZLyrics; using LyricsScraperNET.Providers.Genius; +using LyricsScraperNET.Providers.LyricFind; using LyricsScraperNET.Providers.Musixmatch; using LyricsScraperNET.Providers.SongLyrics; @@ -30,5 +31,11 @@ public static ILyricsScraperClient WithSongLyrics(this ILyricsScraperClient lyri lyricsScraperClient.AddProvider(new SongLyricsProvider()); return lyricsScraperClient; } + + public static ILyricsScraperClient WithLyricFind(this ILyricsScraperClient lyricsScraperClient) + { + lyricsScraperClient.AddProvider(new LyricFindProvider()); + return lyricsScraperClient; + } } } diff --git a/LyricsScraperNET/Extensions/StringExtensions.cs b/LyricsScraperNET/Extensions/StringExtensions.cs index 1c07c4d..4fc1303 100644 --- a/LyricsScraperNET/Extensions/StringExtensions.cs +++ b/LyricsScraperNET/Extensions/StringExtensions.cs @@ -1,5 +1,6 @@ using System; using System.Linq; +using System.Text.RegularExpressions; namespace LyricsScraperNET.Extensions { @@ -12,7 +13,9 @@ public static string RemoveHtmlTags(this string text) { if (string.IsNullOrWhiteSpace(text)) return text; + int idx = text.IndexOf('<'); + while (idx >= 0) { var endIdx = text.IndexOf('>', idx + 1); @@ -23,6 +26,7 @@ public static string RemoveHtmlTags(this string text) text = text.Remove(idx, endIdx - idx + 1); idx = text.IndexOf('<', idx); } + return text; } @@ -30,7 +34,9 @@ public static string StripRedundantChars(this string input, bool removeArticle = { if (string.IsNullOrWhiteSpace(input)) return input; + var result = input.ToLowerInvariant().Trim(); + if (removeArticle) foreach (var article in ARTICLES) { @@ -40,19 +46,29 @@ public static string StripRedundantChars(this string input, bool removeArticle = break; } } + result = new string(result.Where(c => char.IsLetterOrDigit(c)).ToArray()); + return result; } - public static string СonvertToDashedFormat(this string input, bool useExceptionSymbols = true) + public static string СonvertToDashedFormat(this string input, bool useExceptionSymbols = true, bool removeProhibitedSymbols = false) { if (string.IsNullOrWhiteSpace(input)) return input; + var result = input.ToLowerInvariant().Trim(); - result = new string(result.Select(x => + + if (removeProhibitedSymbols) + result = new string(result.Where(x => char.IsLetterOrDigit(x) || char.IsWhiteSpace(x) || x == '-' || x == '.').ToArray()); + + result = Regex.Replace(new string(result.Select(x => { - return (char.IsLetterOrDigit(x) || (useExceptionSymbols && EXCEPTION_SYMBOLS.Contains(x))) ? x : '-'; - }).ToArray()).Replace("--", "-").Trim('-'); + return (char.IsLetterOrDigit(x) || (useExceptionSymbols && EXCEPTION_SYMBOLS.Contains(x))) + ? x + : '-'; + }).ToArray()), "-+", "-").Trim('-'); + return result; } } diff --git a/LyricsScraperNET/ILyricsScraperClient.cs b/LyricsScraperNET/ILyricsScraperClient.cs index 0cc5ed1..e493176 100644 --- a/LyricsScraperNET/ILyricsScraperClient.cs +++ b/LyricsScraperNET/ILyricsScraperClient.cs @@ -1,18 +1,53 @@ using LyricsScraperNET.Models.Requests; using LyricsScraperNET.Models.Responses; using LyricsScraperNET.Providers.Abstract; +using LyricsScraperNET.Providers.Models; using System.Threading.Tasks; namespace LyricsScraperNET { public interface ILyricsScraperClient { + /// + /// Checking that there is some enabled external provider available for search. + /// bool IsEnabled { get; } + IExternalProvider this[ExternalProviderType providerType] { get; } + + /// + /// Search lyric by different search requests: + /// 1) Search by Uri: + /// 2) Search by Artist and Song name: + /// SearchResult SearchLyric(SearchRequest searchRequest); + /// + /// Async search lyric by different search requests: + /// 1) Search by Uri: + /// 2) Search by Artist and Song name: + /// Task SearchLyricAsync(SearchRequest searchRequest); + /// + /// Adding a new external provider that will be used to search for lyrics. + /// void AddProvider(IExternalProvider provider); + + /// + /// Removing an external provider by from the list of search providers. + /// + void RemoveProvider(ExternalProviderType providerType); + + /// + /// Enable lyrics search. All external providers will be enabled in this case. + /// + void Enable(); + + /// + /// Disable lyrics search. All external providers will be enabled in this case. + /// Calling the lyrics search method will return an empty result. + /// + void Disable(); } } diff --git a/LyricsScraperNET/LyricsScraperClient.cs b/LyricsScraperNET/LyricsScraperClient.cs index 7f6cbaf..4a7e9e0 100644 --- a/LyricsScraperNET/LyricsScraperClient.cs +++ b/LyricsScraperNET/LyricsScraperClient.cs @@ -3,6 +3,7 @@ using LyricsScraperNET.Models.Requests; using LyricsScraperNET.Models.Responses; using LyricsScraperNET.Providers.Abstract; +using LyricsScraperNET.Providers.Models; using Microsoft.Extensions.Logging; using System.Collections.Generic; using System.Linq; @@ -15,11 +16,18 @@ public sealed class LyricsScraperClient : ILyricsScraperClient private readonly ILogger _logger; - private IList _externalProviders; + private List _externalProviders; private readonly ILyricScraperClientConfig _lyricScraperClientConfig; public bool IsEnabled => _externalProviders != null && _externalProviders.Any(x => x.IsEnabled); + public IExternalProvider this[ExternalProviderType providerType] + { + get => !IsEmptyProviders() + ? _externalProviders.FirstOrDefault(p => p.Options.ExternalProviderType == providerType) + : null; + } + public LyricsScraperClient() { } public LyricsScraperClient(ILyricScraperClientConfig lyricScraperClientConfig, @@ -88,7 +96,7 @@ private bool ValidateRequest() else if (!IsEnabled) { error = "All external providers is disabled. Searching lyrics is disabled."; - logLevel = LogLevel.Warning; + logLevel = LogLevel.Debug; } if (!string.IsNullOrWhiteSpace(error)) @@ -110,5 +118,35 @@ public void AddProvider(IExternalProvider provider) } private bool IsEmptyProviders() => _externalProviders == null || !_externalProviders.Any(); + + public void RemoveProvider(ExternalProviderType providerType) + { + if (IsEmptyProviders()) + return; + + _externalProviders.RemoveAll(x => x.Options.ExternalProviderType == providerType); + } + + public void Enable() + { + if (IsEmptyProviders()) + return; + + foreach (var provider in _externalProviders) + { + provider.Enable(); + } + } + + public void Disable() + { + if (IsEmptyProviders()) + return; + + foreach (var provider in _externalProviders) + { + provider.Disable(); + } + } } } \ No newline at end of file diff --git a/LyricsScraperNET/Providers/AZLyrics/AZLyricsOptions.cs b/LyricsScraperNET/Providers/AZLyrics/AZLyricsOptions.cs index 6e4d13b..115ded2 100644 --- a/LyricsScraperNET/Providers/AZLyrics/AZLyricsOptions.cs +++ b/LyricsScraperNET/Providers/AZLyrics/AZLyricsOptions.cs @@ -10,7 +10,7 @@ public sealed class AZLyricsOptions : IExternalProviderOptions public ExternalProviderType ExternalProviderType => ExternalProviderType.AZLyrics; - public int SearchPriority { get; set; } = 3; + public int SearchPriority { get; set; } = 4; public const string ConfigurationSectionName = "AZLyricsOptions"; diff --git a/LyricsScraperNET/Providers/AZLyrics/AZLyricsProvider.cs b/LyricsScraperNET/Providers/AZLyrics/AZLyricsProvider.cs index d83fceb..385fa97 100644 --- a/LyricsScraperNET/Providers/AZLyrics/AZLyricsProvider.cs +++ b/LyricsScraperNET/Providers/AZLyrics/AZLyricsProvider.cs @@ -1,5 +1,4 @@ -using LyricsScraperNET.Extensions; -using LyricsScraperNET.Helpers; +using LyricsScraperNET.Helpers; using LyricsScraperNET.Models.Responses; using LyricsScraperNET.Network; using LyricsScraperNET.Providers.Abstract; @@ -13,41 +12,43 @@ namespace LyricsScraperNET.Providers.AZLyrics public sealed class AZLyricsProvider : ExternalProviderBase { private readonly ILogger _logger; - - private const string _baseUri = "http://www.azlyrics.com/lyrics/"; + private readonly IExternalUriConverter _uriConverter; private const string _lyricStart = ""; private const string _lyricEnd = ""; - public Uri BaseUri => new Uri(_baseUri); + #region Constructors public AZLyricsProvider() { Parser = new AZLyricsParser(); WebClient = new HtmlAgilityWebClient(); Options = new AZLyricsOptions() { Enabled = true }; + _uriConverter = new AZLyricsUriConverter(); } - public AZLyricsProvider(ILogger logger, AZLyricsOptions aZLyricsOptions) : this() + public AZLyricsProvider(ILogger logger, AZLyricsOptions options) : this() { _logger = logger; - Ensure.ArgumentNotNull(aZLyricsOptions, nameof(aZLyricsOptions)); - Options = aZLyricsOptions; + Ensure.ArgumentNotNull(options, nameof(options)); + Options = options; } - public AZLyricsProvider(ILogger logger, IOptionsSnapshot aZLyricsOptions) - : this(logger, aZLyricsOptions.Value) + public AZLyricsProvider(ILogger logger, IOptionsSnapshot options) + : this(logger, options.Value) { - Ensure.ArgumentNotNull(aZLyricsOptions, nameof(aZLyricsOptions)); + Ensure.ArgumentNotNull(options, nameof(options)); } + #endregion + public override IExternalProviderOptions Options { get; } #region Sync protected override SearchResult SearchLyric(string artist, string song) { - return SearchLyric(GetLyricUri(artist, song)); + return SearchLyric(_uriConverter.GetLyricUri(artist, song)); } protected override SearchResult SearchLyric(Uri uri) @@ -63,12 +64,11 @@ protected override SearchResult SearchLyric(Uri uri) #endregion - #region Async protected override async Task SearchLyricAsync(string artist, string song) { - return await SearchLyricAsync(GetLyricUri(artist, song)); + return await SearchLyricAsync(_uriConverter.GetLyricUri(artist, song)); } protected override async Task SearchLyricAsync(Uri uri) @@ -84,16 +84,6 @@ protected override async Task SearchLyricAsync(Uri uri) #endregion - - private Uri GetLyricUri(string artist, string song) - { - // http://www.azlyrics.com/lyrics/youngthug/richniggashit.htm - // remove articles from artist on start. For example for band [The Devil Wears Prada]: https://www.azlyrics.com/d/devilwearsprada.html - var artistStripped = artist.ToLowerInvariant().StripRedundantChars(true); - var titleStripped = song.ToLowerInvariant().StripRedundantChars(); - return new Uri(BaseUri, $"{artistStripped}/{titleStripped}.html"); - } - private SearchResult PostProcessLyric(Uri uri, string text) { if (string.IsNullOrEmpty(text)) diff --git a/LyricsScraperNET/Providers/AZLyrics/AZLyricsUriConverter.cs b/LyricsScraperNET/Providers/AZLyrics/AZLyricsUriConverter.cs new file mode 100644 index 0000000..1e9430d --- /dev/null +++ b/LyricsScraperNET/Providers/AZLyrics/AZLyricsUriConverter.cs @@ -0,0 +1,20 @@ +using LyricsScraperNET.Extensions; +using LyricsScraperNET.Providers.Abstract; +using System; + +namespace LyricsScraperNET.Providers.AZLyrics +{ + internal sealed class AZLyricsUriConverter : IExternalUriConverter + { + private Uri _baseUri => new Uri("http://www.azlyrics.com/lyrics/"); + + public Uri GetLyricUri(string artist, string song) + { + // remove articles from artist on start. For example for band [The Devil Wears Prada]: https://www.azlyrics.com/d/devilwearsprada.html + var artistStripped = artist.ToLowerInvariant().StripRedundantChars(true); + var titleStripped = song.ToLowerInvariant().StripRedundantChars(); + + return new Uri(_baseUri, $"{artistStripped}/{titleStripped}.html"); + } + } +} diff --git a/LyricsScraperNET/Providers/Abstract/ExternalProviderBase.cs b/LyricsScraperNET/Providers/Abstract/ExternalProviderBase.cs index f30d9be..ec0ef29 100644 --- a/LyricsScraperNET/Providers/Abstract/ExternalProviderBase.cs +++ b/LyricsScraperNET/Providers/Abstract/ExternalProviderBase.cs @@ -37,14 +37,10 @@ public virtual SearchResult SearchLyric(SearchRequest searchRequest) } protected virtual SearchResult SearchLyric(Uri uri) - { - throw new NotImplementedException(); - } + => throw new NotImplementedException(); protected virtual SearchResult SearchLyric(string artist, string song) - { - throw new NotImplementedException(); - } + => throw new NotImplementedException(); #endregion @@ -68,14 +64,10 @@ public virtual async Task SearchLyricAsync(SearchRequest searchReq } protected virtual Task SearchLyricAsync(Uri uri) - { - throw new NotImplementedException(); - } + => throw new NotImplementedException(); protected virtual Task SearchLyricAsync(string artist, string song) - { - throw new NotImplementedException(); - } + => throw new NotImplementedException(); #endregion @@ -91,5 +83,17 @@ public void WithWebClient(IWebClient webClient) if (webClient != null) WebClient = webClient; } + + public void Enable() + { + if (Options != null) + Options.Enabled = true; + } + + public void Disable() + { + if (Options != null) + Options.Enabled = false; + } } } diff --git a/LyricsScraperNET/Providers/Abstract/IExternalProvider.cs b/LyricsScraperNET/Providers/Abstract/IExternalProvider.cs index 5645326..d91639f 100644 --- a/LyricsScraperNET/Providers/Abstract/IExternalProvider.cs +++ b/LyricsScraperNET/Providers/Abstract/IExternalProvider.cs @@ -23,5 +23,9 @@ public interface IExternalProvider void WithParser(IExternalProviderLyricParser parser); void WithWebClient(IWebClient webClient); + + void Enable(); + + void Disable(); } } diff --git a/LyricsScraperNET/Providers/Abstract/IExternalUriConverter.cs b/LyricsScraperNET/Providers/Abstract/IExternalUriConverter.cs new file mode 100644 index 0000000..604e8eb --- /dev/null +++ b/LyricsScraperNET/Providers/Abstract/IExternalUriConverter.cs @@ -0,0 +1,9 @@ +using System; + +namespace LyricsScraperNET.Providers.Abstract +{ + internal interface IExternalUriConverter + { + Uri GetLyricUri(string artist, string song); + } +} diff --git a/LyricsScraperNET/Providers/Genius/GeniusProvider.cs b/LyricsScraperNET/Providers/Genius/GeniusProvider.cs index be01780..ef6ba2e 100644 --- a/LyricsScraperNET/Providers/Genius/GeniusProvider.cs +++ b/LyricsScraperNET/Providers/Genius/GeniusProvider.cs @@ -18,34 +18,39 @@ namespace LyricsScraperNET.Providers.Genius public sealed class GeniusProvider : ExternalProviderBase { private readonly ILogger _logger; + private readonly IExternalUriConverter _uriConverter; // Format: "artist song". Example: "Parkway Drive Carrion". private const string GeniusSearchQueryFormat = "{0} {1}"; - private const string GeniusApiSearchFormat = "https://genius.com/api/search?q={0}"; private const string _referentFragmentNodesXPath = "//a[contains(@class, 'ReferentFragmentVariantdesktop') or contains(@class, 'ReferentFragmentdesktop')]"; private const string _lyricsContainerNodesXPath = "//div[@data-lyrics-container]"; + #region Constructors + public GeniusProvider() { Parser = new GeniusParser(); WebClient = new NetHttpClient(); Options = new GeniusOptions() { Enabled = true }; + _uriConverter = new GeniusUriConverter(); } - public GeniusProvider(ILogger logger, GeniusOptions geniusOptions) : this() + public GeniusProvider(ILogger logger, GeniusOptions options) : this() { _logger = logger; - Ensure.ArgumentNotNull(geniusOptions, nameof(geniusOptions)); - Options = geniusOptions; + Ensure.ArgumentNotNull(options, nameof(options)); + Options = options; } - public GeniusProvider(ILogger logger, IOptionsSnapshot geniusOptions) - : this(logger, geniusOptions.Value) + public GeniusProvider(ILogger logger, IOptionsSnapshot options) + : this(logger, options.Value) { - Ensure.ArgumentNotNull(geniusOptions, nameof(geniusOptions)); + Ensure.ArgumentNotNull(options, nameof(options)); } + #endregion + public override IExternalProviderOptions Options { get; } #region Sync @@ -82,7 +87,6 @@ protected override SearchResult SearchLyric(string artist, string song) #endregion - #region Async protected override async Task SearchLyricAsync(Uri uri) @@ -119,7 +123,7 @@ protected override async Task SearchLyricAsync(string artist, stri private string GetLyricUrlWithoutApiKey(string artist, string song) { - var htmlPageBody = WebClient.Load(new Uri(GetApiSearchUrl(artist, song))); + var htmlPageBody = WebClient.Load(_uriConverter.GetLyricUri(artist, song)); if (string.IsNullOrWhiteSpace(htmlPageBody)) return string.Empty; @@ -187,8 +191,5 @@ private string GetLyricUrlFromSearchResponse(SearchResponse searchResponse, stri private string GetApiSearchQuery(string artist, string song) => string.Format(GeniusSearchQueryFormat, artist, song); - - private string GetApiSearchUrl(string artist, string song) - => string.Format(GeniusApiSearchFormat, GetApiSearchQuery(artist, song)); } } diff --git a/LyricsScraperNET/Providers/Genius/GeniusUriConverter.cs b/LyricsScraperNET/Providers/Genius/GeniusUriConverter.cs new file mode 100644 index 0000000..2246fce --- /dev/null +++ b/LyricsScraperNET/Providers/Genius/GeniusUriConverter.cs @@ -0,0 +1,19 @@ +using LyricsScraperNET.Providers.Abstract; +using System; + +namespace LyricsScraperNET.Providers.Genius +{ + internal sealed class GeniusUriConverter : IExternalUriConverter + { + // Format: "artist song". Example: "Parkway Drive Carrion". + private const string GeniusSearchQueryFormat = "{0} {1}"; + private const string GeniusApiSearchFormat = "https://genius.com/api/search?q={0}"; + + private string GetApiSearchQuery(string artist, string song) + => string.Format(GeniusSearchQueryFormat, artist, song); + + public Uri GetLyricUri(string artist, string song) + => new Uri(string.Format(GeniusApiSearchFormat, GetApiSearchQuery(artist, song))); + + } +} diff --git a/LyricsScraperNET/Providers/LyricFind/LyricFindOptions.cs b/LyricsScraperNET/Providers/LyricFind/LyricFindOptions.cs new file mode 100644 index 0000000..16d88fc --- /dev/null +++ b/LyricsScraperNET/Providers/LyricFind/LyricFindOptions.cs @@ -0,0 +1,28 @@ +using LyricsScraperNET.Providers.Abstract; +using LyricsScraperNET.Providers.Models; +using System; + +namespace LyricsScraperNET.Providers.LyricFind +{ + public sealed class LyricFindOptions : IExternalProviderOptions + { + public bool Enabled { get; set; } + + public ExternalProviderType ExternalProviderType => ExternalProviderType.LyricFind; + + public int SearchPriority { get; set; } = 3; + + public const string ConfigurationSectionName = "LyricFindOptions"; + + public override bool Equals(object? obj) + { + return obj is LyricFindOptions options && + ExternalProviderType == options.ExternalProviderType; + } + + public override int GetHashCode() + { + return HashCode.Combine(ExternalProviderType); + } + } +} diff --git a/LyricsScraperNET/Providers/LyricFind/LyricFindParser.cs b/LyricsScraperNET/Providers/LyricFind/LyricFindParser.cs new file mode 100644 index 0000000..d4bd12e --- /dev/null +++ b/LyricsScraperNET/Providers/LyricFind/LyricFindParser.cs @@ -0,0 +1,46 @@ +using LyricsScraperNET.Extensions; +using LyricsScraperNET.Providers.Abstract; + +namespace LyricsScraperNET.Providers.LyricFind +{ + internal sealed class LyricFindParser : IExternalProviderLyricParser + { + public string Parse(string lyric) + { + return UnescapeString(RemoveAllHtmlTags(lyric))?.Trim()?.Replace("\\n", "\r\n"); + } + + private string RemoveAllHtmlTags(string html) + { + html = html.RemoveHtmlTags(); + + // fix recursive white-spaces + while (html.Contains(" ")) + { + html = html.Replace(" ", " "); + } + + // fix recursive line-break + while (html.Contains("\r\n\r\n\r\n")) + { + html = html.Replace("\r\n\r\n\r\n", "\r\n\r\n"); + } + + return html; + } + + private string UnescapeString(string lyric) + { + if (!string.IsNullOrEmpty(lyric)) + { + // replace entities with literal values + lyric = lyric.Replace("'", "'"); + lyric = lyric.Replace(""", "\""); + lyric = lyric.Replace(">", ">"); + lyric = lyric.Replace("<", "<"); + lyric = lyric.Replace("&", "&"); + } + return lyric; + } + } +} diff --git a/LyricsScraperNET/Providers/LyricFind/LyricFindProvider.cs b/LyricsScraperNET/Providers/LyricFind/LyricFindProvider.cs new file mode 100644 index 0000000..482f31e --- /dev/null +++ b/LyricsScraperNET/Providers/LyricFind/LyricFindProvider.cs @@ -0,0 +1,121 @@ +using LyricsScraperNET.Helpers; +using LyricsScraperNET.Models.Responses; +using LyricsScraperNET.Network; +using LyricsScraperNET.Providers.Abstract; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using System; +using System.Threading.Tasks; + +namespace LyricsScraperNET.Providers.LyricFind +{ + public sealed class LyricFindProvider : ExternalProviderBase + { + private readonly ILogger _logger; + private readonly IExternalUriConverter _uriConverter; + + private const string _lyricStart = "\"lyrics\""; + + #region Constructors + + public LyricFindProvider() + { + Parser = new LyricFindParser(); + WebClient = new HtmlAgilityWebClient(); + Options = new LyricFindOptions() { Enabled = true }; + _uriConverter = new LyricFindUriConverter(); + } + + public LyricFindProvider(ILogger logger, LyricFindOptions options) : this() + { + _logger = logger; + Ensure.ArgumentNotNull(options, nameof(options)); + Options = options; + } + + public LyricFindProvider(ILogger logger, IOptionsSnapshot options) + : this(logger, options.Value) + { + Ensure.ArgumentNotNull(options, nameof(options)); + } + + #endregion + + public override IExternalProviderOptions Options { get; } + + #region Sync + + protected override SearchResult SearchLyric(string artist, string song) + { + return SearchLyric(_uriConverter.GetLyricUri(artist, song)); + } + + protected override SearchResult SearchLyric(Uri uri) + { + if (WebClient == null || Parser == null) + { + _logger?.LogWarning($"LyricFind. Please set up WebClient and Parser first"); + return new SearchResult(); + } + var text = WebClient.Load(uri); + return PostProcessLyric(uri, text); + } + + #endregion + + #region Async + + protected override async Task SearchLyricAsync(string artist, string song) + { + return await SearchLyricAsync(_uriConverter.GetLyricUri(artist, song)); + } + + protected override async Task SearchLyricAsync(Uri uri) + { + if (WebClient == null || Parser == null) + { + _logger?.LogWarning($"LyricFind. Please set up WebClient and Parser first"); + return new SearchResult(); + } + var text = await WebClient.LoadAsync(uri); + return PostProcessLyric(uri, text); + } + + #endregion + + private SearchResult PostProcessLyric(Uri uri, string text) + { + if (string.IsNullOrEmpty(text)) + { + _logger?.LogWarning($"LyricFind. Text is empty for {uri}"); + return new SearchResult(); + } + + var startIndex = text.IndexOf(_lyricStart); + if (startIndex <= 0) + { + _logger?.LogWarning($"LyricFind. Can't find lyrics for {uri}"); + return new SearchResult(); + } + + // Trim the beginning of the text to the lyrics + text = text.Substring(startIndex + _lyricStart.Length + 1); + + // Finding the end of the lyric text in the json field value. + int start = text.IndexOf("\"") + 1; + + int endOfFieldValue = text.IndexOf("\",\"", start); + int endOfJsonObject = text.IndexOf("\"}", start); + int endOfLyricInJson = Math.Max(Math.Min(endOfFieldValue, endOfJsonObject), -1); + if (endOfLyricInJson < 0) + { + _logger?.LogWarning($"LyricFind. Can't parse lyrics for {uri}"); + return new SearchResult(); + } + + string result = Parser.Parse(text.Substring(start, endOfLyricInJson - start)); + + return new SearchResult(result, Models.ExternalProviderType.LyricFind); + } + } +} diff --git a/LyricsScraperNET/Providers/LyricFind/LyricFindUriConverter.cs b/LyricsScraperNET/Providers/LyricFind/LyricFindUriConverter.cs new file mode 100644 index 0000000..ef90234 --- /dev/null +++ b/LyricsScraperNET/Providers/LyricFind/LyricFindUriConverter.cs @@ -0,0 +1,20 @@ +using LyricsScraperNET.Extensions; +using LyricsScraperNET.Providers.Abstract; +using System; + +namespace LyricsScraperNET.Providers.LyricFind +{ + internal sealed class LyricFindUriConverter : IExternalUriConverter + { + // 0 - artist, 1 - song + private const string uriPathFormat = "https://lyrics.lyricfind.com/lyrics/{0}-{1}"; + + public Uri GetLyricUri(string artist, string song) + { + var artistFormatted = artist.ToLowerInvariant().СonvertToDashedFormat(useExceptionSymbols: false, removeProhibitedSymbols: true); + var songFormatted = song.ToLowerInvariant().СonvertToDashedFormat(useExceptionSymbols: false, removeProhibitedSymbols: true); + + return new Uri(string.Format(uriPathFormat, artistFormatted, songFormatted)); + } + } +} diff --git a/LyricsScraperNET/Providers/Models/ExternalProviderType.cs b/LyricsScraperNET/Providers/Models/ExternalProviderType.cs index fce47f6..03be0b4 100644 --- a/LyricsScraperNET/Providers/Models/ExternalProviderType.cs +++ b/LyricsScraperNET/Providers/Models/ExternalProviderType.cs @@ -6,6 +6,7 @@ public enum ExternalProviderType AZLyrics, Genius, Musixmatch, - SongLyrics + SongLyrics, + LyricFind } } diff --git a/LyricsScraperNET/Providers/Musixmatch/MusixmatchProvider.cs b/LyricsScraperNET/Providers/Musixmatch/MusixmatchProvider.cs index 70bdfd2..49bc688 100644 --- a/LyricsScraperNET/Providers/Musixmatch/MusixmatchProvider.cs +++ b/LyricsScraperNET/Providers/Musixmatch/MusixmatchProvider.cs @@ -22,10 +22,13 @@ public sealed class MusixmatchProvider : ExternalProviderBase // Musixmatch Token memory cache private static readonly IMemoryCache _memoryCache; private static readonly MemoryCacheEntryOptions _memoryCacheEntryOptions; + private static readonly string MusixmatchTokenKey = "MusixmatchToken"; private readonly int _searchRetryAmount = 2; + #region Constructors + static MusixmatchProvider() { _memoryCache = new MemoryCache(new MemoryCacheOptions() @@ -43,61 +46,22 @@ public MusixmatchProvider() Options = new MusixmatchOptions() { Enabled = true }; } - public MusixmatchProvider(ILogger logger, MusixmatchOptions musixmatchOptions) + public MusixmatchProvider(ILogger logger, MusixmatchOptions options) { _logger = logger; - Ensure.ArgumentNotNull(musixmatchOptions, nameof(musixmatchOptions)); - Options = musixmatchOptions; + Ensure.ArgumentNotNull(options, nameof(options)); + Options = options; } - public MusixmatchProvider(ILogger logger, IOptionsSnapshot musixmatchOptions) - : this(logger, musixmatchOptions.Value) + public MusixmatchProvider(ILogger logger, IOptionsSnapshot options) + : this(logger, options.Value) { - Ensure.ArgumentNotNull(musixmatchOptions, nameof(musixmatchOptions)); + Ensure.ArgumentNotNull(options, nameof(options)); } - public override IExternalProviderOptions Options { get; } - - private MusixmatchClient GetMusixmatchClient(bool regenerateToken = false) - { - // TODO: uncomment after the fix of https://github.com/Eimaen/MusixmatchClientLib/issues/21 - //if (Options.TryGetApiKeyFromOptions(out var apiKey)) - //{ - // _logger.LogInformation("Use MusixmatchToken from options."); - // return new MusixmatchClient(apiKey); - //} - //else - //{ - if (regenerateToken) - _memoryCache.Remove(MusixmatchTokenKey); - - _logger?.LogDebug("Musixmatch. Use default MusixmatchToken."); - string 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); - } - (Options as IExternalProviderOptionsWithApiKey).ApiKey = musixmatchTokenValue; - return new MusixmatchClient(musixmatchTokenValue); - //} - } + #endregion - private TrackSearchParameters GetTrackSearchParameters(string artist, string song) - { - return new TrackSearchParameters - { - Artist = artist, - Title = song, // Track name - //Query = $"{artist} - {song}", // Search query, covers all the search parameters above - //HasLyrics = false, // Only search for tracks with lyrics - //HasSubtitles = false, // Only search for tracks with synced lyrics - //Language = "", // Only search for tracks with lyrics in specified language - Sort = TrackSearchParameters.SortStrategy.TrackRatingDesc // List sorting strategy - }; - } + public override IExternalProviderOptions Options { get; } #region Sync @@ -142,7 +106,6 @@ protected override SearchResult SearchLyric(string artist, string song) #endregion - #region Async // TODO: search by uri from the site. Example: https://www.musixmatch.com/lyrics/Parkway-Drive/Idols-and-Anchors @@ -186,5 +149,46 @@ protected override async Task SearchLyricAsync(string artist, stri } #endregion + + private MusixmatchClient GetMusixmatchClient(bool regenerateToken = false) + { + // TODO: uncomment after the fix of https://github.com/Eimaen/MusixmatchClientLib/issues/21 + //if (Options.TryGetApiKeyFromOptions(out var apiKey)) + //{ + // _logger.LogInformation("Use MusixmatchToken from options."); + // return new MusixmatchClient(apiKey); + //} + //else + //{ + if (regenerateToken) + _memoryCache.Remove(MusixmatchTokenKey); + + _logger?.LogDebug("Musixmatch. Use default MusixmatchToken."); + string 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); + } + (Options as IExternalProviderOptionsWithApiKey).ApiKey = musixmatchTokenValue; + return new MusixmatchClient(musixmatchTokenValue); + //} + } + + private TrackSearchParameters GetTrackSearchParameters(string artist, string song) + { + return new TrackSearchParameters + { + Artist = artist, + Title = song, // Track name + //Query = $"{artist} - {song}", // Search query, covers all the search parameters above + //HasLyrics = false, // Only search for tracks with lyrics + //HasSubtitles = false, // Only search for tracks with synced lyrics + //Language = "", // Only search for tracks with lyrics in specified language + Sort = TrackSearchParameters.SortStrategy.TrackRatingDesc // List sorting strategy + }; + } } } diff --git a/LyricsScraperNET/Providers/SongLyrics/SongLyricsProvider.cs b/LyricsScraperNET/Providers/SongLyrics/SongLyricsProvider.cs index 5a3b8c7..6677da4 100644 --- a/LyricsScraperNET/Providers/SongLyrics/SongLyricsProvider.cs +++ b/LyricsScraperNET/Providers/SongLyrics/SongLyricsProvider.cs @@ -1,5 +1,4 @@ using HtmlAgilityPack; -using LyricsScraperNET.Extensions; using LyricsScraperNET.Helpers; using LyricsScraperNET.Models.Responses; using LyricsScraperNET.Network; @@ -15,40 +14,43 @@ namespace LyricsScraperNET.Providers.SongLyrics public sealed class SongLyricsProvider : ExternalProviderBase { private readonly ILogger _logger; + private readonly IExternalUriConverter _uriConverter; - // 0 - artist, 1 - song - private const string SongLyricsUriPathFormat = "https://www.songlyrics.com/{0}/{1}-lyrics/"; private const string _lyricsContainerNodesXPath = "//p[contains(@id, 'songLyricsDiv')]"; private const string NotExistLyricPattern = "We do not have the lyrics for (.*) yet."; + public SongLyricsProvider() { WebClient = new NetHttpClient(); Options = new SongLyricsOptions() { Enabled = true }; + _uriConverter = new SongLyricsUriConverter(); } - public SongLyricsProvider(ILogger logger, SongLyricsOptions songLyricsOptions) : this() + public SongLyricsProvider(ILogger logger, SongLyricsOptions options) : this() { _logger = logger; - Ensure.ArgumentNotNull(songLyricsOptions, nameof(songLyricsOptions)); - Options = songLyricsOptions; + Ensure.ArgumentNotNull(options, nameof(options)); + Options = options; } - public SongLyricsProvider(ILogger logger, IOptionsSnapshot songLyricsOptions) - : this(logger, songLyricsOptions.Value) + public SongLyricsProvider(ILogger logger, IOptionsSnapshot options) + : this(logger, options.Value) { - Ensure.ArgumentNotNull(songLyricsOptions, nameof(songLyricsOptions)); + Ensure.ArgumentNotNull(options, nameof(options)); } + public override IExternalProviderOptions Options { get; } + #region Sync protected override SearchResult SearchLyric(string artist, string song) { - return SearchLyric(GetLyricUri(artist, song)); + return SearchLyric(_uriConverter.GetLyricUri(artist, song)); } protected override SearchResult SearchLyric(Uri uri) @@ -69,7 +71,7 @@ protected override SearchResult SearchLyric(Uri uri) protected override async Task SearchLyricAsync(string artist, string song) { - return await SearchLyricAsync(GetLyricUri(artist, song)); + return await SearchLyricAsync(_uriConverter.GetLyricUri(artist, song)); } protected override async Task SearchLyricAsync(Uri uri) @@ -86,16 +88,6 @@ protected override async Task SearchLyricAsync(Uri uri) #endregion - private Uri GetLyricUri(string artist, string song) - { - // Attack Attack! - I Swear I'll Change -> https://www.songlyrics.com/attack-attack!/i-swear-i-ll-change-lyrics/ - // Against Me! - Stop! -> https://www.songlyrics.com/against-me!/stop!-lyrics/ - // The Devil Wears Prada - You Can't Spell Crap Without "C" -> https://www.songlyrics.com/the-devil-wears-prada/you-can-t-spell-crap-without-c-lyrics/ - var artistFormatted = artist.ToLowerInvariant().СonvertToDashedFormat(); - var songFormatted = song.ToLowerInvariant().СonvertToDashedFormat(); - return new Uri(string.Format(SongLyricsUriPathFormat, artistFormatted, songFormatted)); - } - private SearchResult GetParsedLyricFromHtmlPageBody(Uri uri, string htmlPageBody) { if (string.IsNullOrEmpty(htmlPageBody)) diff --git a/LyricsScraperNET/Providers/SongLyrics/SongLyricsUriConverter.cs b/LyricsScraperNET/Providers/SongLyrics/SongLyricsUriConverter.cs new file mode 100644 index 0000000..8d25895 --- /dev/null +++ b/LyricsScraperNET/Providers/SongLyrics/SongLyricsUriConverter.cs @@ -0,0 +1,20 @@ +using LyricsScraperNET.Extensions; +using LyricsScraperNET.Providers.Abstract; +using System; + +namespace LyricsScraperNET.Providers.SongLyrics +{ + internal sealed class SongLyricsUriConverter : IExternalUriConverter + { + // 0 - artist, 1 - song + private const string uriPathFormat = "https://www.songlyrics.com/{0}/{1}-lyrics/"; + + public Uri GetLyricUri(string artist, string song) + { + var artistFormatted = artist.ToLowerInvariant().СonvertToDashedFormat(); + var songFormatted = song.ToLowerInvariant().СonvertToDashedFormat(); + + return new Uri(string.Format(uriPathFormat, artistFormatted, songFormatted)); + } + } +} diff --git a/Tests/LyricsScraperNET.IntegrationTest/LyricsScraperNET.IntegrationTest.csproj b/Tests/LyricsScraperNET.IntegrationTest/LyricsScraperNET.IntegrationTest.csproj index 477e7c9..9bb552d 100644 --- a/Tests/LyricsScraperNET.IntegrationTest/LyricsScraperNET.IntegrationTest.csproj +++ b/Tests/LyricsScraperNET.IntegrationTest/LyricsScraperNET.IntegrationTest.csproj @@ -37,6 +37,12 @@ PreserveNewest + + PreserveNewest + + + PreserveNewest + PreserveNewest diff --git a/Tests/LyricsScraperNET.IntegrationTest/Providers/LyricFind/LyricFindProviderTest.cs b/Tests/LyricsScraperNET.IntegrationTest/Providers/LyricFind/LyricFindProviderTest.cs new file mode 100644 index 0000000..3d741d3 --- /dev/null +++ b/Tests/LyricsScraperNET.IntegrationTest/Providers/LyricFind/LyricFindProviderTest.cs @@ -0,0 +1,30 @@ +using LyricsScraperNET.Models.Requests; +using LyricsScraperNET.Providers.LyricFind; +using LyricsScraperNET.Providers.Models; +using LyricsScraperNET.TestShared.Providers; +using LyricsScraperNET.UnitTest.TestModel; +using Xunit; + +namespace LyricsScraperNET.IntegrationTest.Providers.LyricFind +{ + public class LyricFindProviderTest : ProviderTestBase + { + [Theory] + [MemberData(nameof(GetTestData), parameters: "Providers\\LyricFind\\test_data.json")] + public void SearchLyric_IntegrationDynamicData_AreEqual(LyricsTestData testData) + { + // Arrange + var lyricsClient = new LyricFindProvider(); + SearchRequest searchRequest = CreateSearchRequest(testData); + + // Act + var searchResult = lyricsClient.SearchLyric(searchRequest); + + // Assert + Assert.NotNull(searchResult); + Assert.False(searchResult.IsEmpty()); + Assert.Equal(ExternalProviderType.LyricFind, searchResult.ExternalProviderType); + Assert.Equal(testData.LyricResultData.Replace("\r\n", "\n"), searchResult.LyricText.Replace("\r\n", "\n")); + } + } +} diff --git a/Tests/LyricsScraperNET.IntegrationTest/Providers/LyricFind/Resources/Lyrics_Result_01.txt b/Tests/LyricsScraperNET.IntegrationTest/Providers/LyricFind/Resources/Lyrics_Result_01.txt new file mode 100644 index 0000000..cd67acd --- /dev/null +++ b/Tests/LyricsScraperNET.IntegrationTest/Providers/LyricFind/Resources/Lyrics_Result_01.txt @@ -0,0 +1,21 @@ +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 when there's nothing left for you to cling to? +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 +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 +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/LyricFind/test_data.json b/Tests/LyricsScraperNET.IntegrationTest/Providers/LyricFind/test_data.json new file mode 100644 index 0000000..8316bec --- /dev/null +++ b/Tests/LyricsScraperNET.IntegrationTest/Providers/LyricFind/test_data.json @@ -0,0 +1,8 @@ +[ + { + "LyricResultPath": "Providers/LyricFind/Resources/Lyrics_Result_01.txt", + "ArtistName": "Parkway Drive", + "SongName": "Idols And Anchors", + "SongUri": null + } +] \ No newline at end of file diff --git a/Tests/LyricsScraperNET.UnitTest/Configuration/ServiceCollectionExtensionsTest.cs b/Tests/LyricsScraperNET.UnitTest/Configuration/ServiceCollectionExtensionsTest.cs index ab68bb7..4da9521 100644 --- a/Tests/LyricsScraperNET.UnitTest/Configuration/ServiceCollectionExtensionsTest.cs +++ b/Tests/LyricsScraperNET.UnitTest/Configuration/ServiceCollectionExtensionsTest.cs @@ -1,7 +1,12 @@ using LyricsScraperNET.Configuration; +using LyricsScraperNET.Models.Requests; +using LyricsScraperNET.Providers.Models; using LyricsScraperNET.TestShared.Helpers; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using System; +using System.Collections.Generic; +using System.Linq; using Xunit; namespace LyricsScraperNET.UnitTest.Configuration @@ -30,16 +35,34 @@ public void IocContainer_GetService_LyricsScraperClient_DefaultNotNull() var serviceProvider = serviceCollection.BuildServiceProvider(); var service = serviceProvider.GetService(); + var searchResult = service.SearchLyric(new ArtistAndSongSearchRequest(null, null)); // Assert Assert.NotNull(service); Assert.False(service.IsEnabled); + Assert.NotNull(searchResult); + Assert.True(searchResult.IsEmpty()); + Assert.True(searchResult.ExternalProviderType == ExternalProviderType.None); + Assert.True(string.IsNullOrEmpty(searchResult.LyricText)); } [Fact] public void IocContainer_GetService_LyricsScraperClient_FullSetup() { // Arrange + var externalProviderTypes = Enum.GetValues(typeof(ExternalProviderType)) + .Cast() + .Where(p => p != ExternalProviderType.None); + + Dictionary expectedSearchPriorities = new Dictionary() + { + { ExternalProviderType.Genius, 11}, + { ExternalProviderType.AZLyrics, 22}, + { ExternalProviderType.Musixmatch, 33}, + { ExternalProviderType.SongLyrics, 44}, + { ExternalProviderType.LyricFind, 55} + }; + string settingsPath = "Resources\\full_test_settings.json"; var serviceCollection = new ServiceCollection(); @@ -54,6 +77,12 @@ public void IocContainer_GetService_LyricsScraperClient_FullSetup() // Assert Assert.NotNull(service); Assert.True(service.IsEnabled); + + foreach (var providerType in externalProviderTypes) + { + Assert.True(service[providerType].IsEnabled); + Assert.Equal(expectedSearchPriorities[providerType], service[providerType].SearchPriority); + } } } } diff --git a/Tests/LyricsScraperNET.UnitTest/Extensions/LyricsScraperClientExtensionsTest.cs b/Tests/LyricsScraperNET.UnitTest/Extensions/LyricsScraperClientExtensionsTest.cs index 35c6ce8..11c8504 100644 --- a/Tests/LyricsScraperNET.UnitTest/Extensions/LyricsScraperClientExtensionsTest.cs +++ b/Tests/LyricsScraperNET.UnitTest/Extensions/LyricsScraperClientExtensionsTest.cs @@ -1,4 +1,5 @@ -using Xunit; +using LyricsScraperNET.Providers.Models; +using Xunit; namespace LyricsScraperNET.UnitTest.Extensions { @@ -11,10 +12,14 @@ public void LyricsScraperClient_WithAZLyrics_ReturnsIsEnabled() { // Act var lyricsScraperClient = _lyricsScraperClient.WithAZLyrics(); + var externalTypeProvider = lyricsScraperClient[ExternalProviderType.AZLyrics]; // Assert Assert.NotNull(lyricsScraperClient); Assert.True(lyricsScraperClient.IsEnabled); + Assert.NotNull(externalTypeProvider); + Assert.True(externalTypeProvider.IsEnabled); + Assert.Equal(4, externalTypeProvider.SearchPriority); } [Fact] @@ -22,10 +27,14 @@ public void LyricsScraperClient_WithGenius_ReturnsIsEnabled() { // Act var lyricsScraperClient = _lyricsScraperClient.WithGenius(); + var externalTypeProvider = lyricsScraperClient[ExternalProviderType.Genius]; // Assert Assert.NotNull(lyricsScraperClient); Assert.True(lyricsScraperClient.IsEnabled); + Assert.NotNull(externalTypeProvider); + Assert.True(externalTypeProvider.IsEnabled); + Assert.Equal(1, externalTypeProvider.SearchPriority); } [Fact] @@ -33,10 +42,14 @@ public void LyricsScraperClient_WithMusixmatch_ReturnsIsEnabled() { // Act var lyricsScraperClient = _lyricsScraperClient.WithMusixmatch(); + var externalTypeProvider = lyricsScraperClient[ExternalProviderType.Musixmatch]; // Assert Assert.NotNull(lyricsScraperClient); Assert.True(lyricsScraperClient.IsEnabled); + Assert.NotNull(externalTypeProvider); + Assert.True(externalTypeProvider.IsEnabled); + Assert.Equal(2, externalTypeProvider.SearchPriority); } [Fact] @@ -44,10 +57,29 @@ public void LyricsScraperClient_WithSongLyrics_ReturnsIsEnabled() { // Act var lyricsScraperClient = _lyricsScraperClient.WithSongLyrics(); + var externalTypeProvider = lyricsScraperClient[ExternalProviderType.SongLyrics]; // Assert Assert.NotNull(lyricsScraperClient); Assert.True(lyricsScraperClient.IsEnabled); + Assert.NotNull(externalTypeProvider); + Assert.True(externalTypeProvider.IsEnabled); + Assert.Equal(0, externalTypeProvider.SearchPriority); + } + + [Fact] + public void LyricsScraperClient_WithLyricFind_ReturnsIsEnabled() + { + // Act + var lyricsScraperClient = _lyricsScraperClient.WithLyricFind(); + var externalTypeProvider = lyricsScraperClient[ExternalProviderType.LyricFind]; + + // Assert + Assert.NotNull(lyricsScraperClient); + Assert.True(lyricsScraperClient.IsEnabled); + Assert.NotNull(externalTypeProvider); + Assert.True(externalTypeProvider.IsEnabled); + Assert.Equal(3, externalTypeProvider.SearchPriority); } } } diff --git a/Tests/LyricsScraperNET.UnitTest/Extensions/StringExtensionsTest.cs b/Tests/LyricsScraperNET.UnitTest/Extensions/StringExtensionsTest.cs index 863e8c6..e4a20ea 100644 --- a/Tests/LyricsScraperNET.UnitTest/Extensions/StringExtensionsTest.cs +++ b/Tests/LyricsScraperNET.UnitTest/Extensions/StringExtensionsTest.cs @@ -9,6 +9,8 @@ public class StringExtensionsTest [InlineData("Attack Attack!", "attack-attack!")] [InlineData("I Swear I'll Change", "i-swear-i-ll-change")] [InlineData("You Can't Spell Crap Without \"C\"", "you-can-t-spell-crap-without-c")] + [InlineData("Summer of '69", "summer-of-69")] + [InlineData(" Of Mice & Men ", "of-mice-men")] [InlineData("", "")] [InlineData(null, null)] public void СonvertToDashedFormat_MultipleInputs_ShouldBeParse(string input, string expected) diff --git a/Tests/LyricsScraperNET.UnitTest/LyricsScraperNET.UnitTest.csproj b/Tests/LyricsScraperNET.UnitTest/LyricsScraperNET.UnitTest.csproj index 1391859..82547a7 100644 --- a/Tests/LyricsScraperNET.UnitTest/LyricsScraperNET.UnitTest.csproj +++ b/Tests/LyricsScraperNET.UnitTest/LyricsScraperNET.UnitTest.csproj @@ -49,6 +49,15 @@ PreserveNewest + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + PreserveNewest diff --git a/Tests/LyricsScraperNET.UnitTest/Providers/AZLyrics/AZLyricsUriConverterTests.cs b/Tests/LyricsScraperNET.UnitTest/Providers/AZLyrics/AZLyricsUriConverterTests.cs new file mode 100644 index 0000000..b1d465b --- /dev/null +++ b/Tests/LyricsScraperNET.UnitTest/Providers/AZLyrics/AZLyricsUriConverterTests.cs @@ -0,0 +1,24 @@ +using LyricsScraperNET.Providers.AZLyrics; +using System; +using Xunit; + +namespace LyricsScraperNET.UnitTest.Providers.AZLyrics +{ + public class AZLyricsUriConverterTests + { + [Theory] + [InlineData("The Devil Wears Prada", "You Can't Spell Crap Without 'C'", "http://www.azlyrics.com/lyrics/devilwearsprada/youcantspellcrapwithoutc.html")] + [InlineData(" Young Thug ", " Rich Nigga Shit ", "http://www.azlyrics.com/lyrics/youngthug/richniggashit.html")] + public void GetLyricUri_MultipleInputs_ShouldBeParse(string artistName, string songName, string expectedUri) + { + // Arrange + var uriConverter = new AZLyricsUriConverter(); + + // Act + var actual = uriConverter.GetLyricUri(artistName, songName); + + // Assert + Assert.Equal(new Uri(expectedUri), actual); + } + } +} diff --git a/Tests/LyricsScraperNET.UnitTest/Providers/Genius/GeniusUriConverterTests.cs b/Tests/LyricsScraperNET.UnitTest/Providers/Genius/GeniusUriConverterTests.cs new file mode 100644 index 0000000..8d08ef3 --- /dev/null +++ b/Tests/LyricsScraperNET.UnitTest/Providers/Genius/GeniusUriConverterTests.cs @@ -0,0 +1,23 @@ +using LyricsScraperNET.Providers.Genius; +using System; +using Xunit; + +namespace LyricsScraperNET.UnitTest.Providers.Genius +{ + public class GeniusUriConverterTests + { + [Theory] + [InlineData("Parkway Drive", "Carrion", "https://genius.com/api/search?q=Parkway Drive Carrion")] + public void GetLyricUri_MultipleInputs_ShouldBeParse(string artistName, string songName, string expectedUri) + { + // Arrange + var uriConverter = new GeniusUriConverter(); + + // Act + var actual = uriConverter.GetLyricUri(artistName, songName); + + // Assert + Assert.Equal(new Uri(expectedUri), actual); + } + } +} diff --git a/Tests/LyricsScraperNET.UnitTest/Providers/LyricFind/LyricFindProviderTest.cs b/Tests/LyricsScraperNET.UnitTest/Providers/LyricFind/LyricFindProviderTest.cs new file mode 100644 index 0000000..4ab77f7 --- /dev/null +++ b/Tests/LyricsScraperNET.UnitTest/Providers/LyricFind/LyricFindProviderTest.cs @@ -0,0 +1,32 @@ +using LyricsScraperNET.Models.Requests; +using LyricsScraperNET.Providers.LyricFind; +using LyricsScraperNET.Providers.Models; +using LyricsScraperNET.TestShared.Extensions; +using LyricsScraperNET.TestShared.Providers; +using LyricsScraperNET.UnitTest.TestModel; +using Xunit; + +namespace LyricsScraperNET.UnitTest.Providers.LyricFind +{ + public class LyricFindProviderTest : ProviderTestBase + { + [Theory] + [MemberData(nameof(GetTestData), parameters: "Providers\\LyricFind\\test_data.json")] + public void SearchLyric_UnitDynamicData_AreEqual(LyricsTestData testData) + { + // Arrange + var lyricsClient = new LyricFindProvider(); + lyricsClient.ConfigureExternalProvider(testData); + + SearchRequest searchRequest = CreateSearchRequest(testData); + + // Act + var searchResult = lyricsClient.SearchLyric(searchRequest); + + // Assert + Assert.NotNull(searchResult); + Assert.Equal(ExternalProviderType.LyricFind, searchResult.ExternalProviderType); + Assert.Equal(testData.LyricResultData.Replace("\r\n", "\n"), searchResult.LyricText.Replace("\r\n", "\n")); + } + } +} diff --git a/Tests/LyricsScraperNET.UnitTest/Providers/LyricFind/LyricFindUriConverterTests.cs b/Tests/LyricsScraperNET.UnitTest/Providers/LyricFind/LyricFindUriConverterTests.cs new file mode 100644 index 0000000..8b95a7b --- /dev/null +++ b/Tests/LyricsScraperNET.UnitTest/Providers/LyricFind/LyricFindUriConverterTests.cs @@ -0,0 +1,30 @@ +using LyricsScraperNET.Providers.LyricFind; +using System; +using Xunit; + +namespace LyricsScraperNET.UnitTest.Providers.LyricFind +{ + public class LyricFindUriConverterTests + { + [Theory] + [InlineData("Bryan Adams", "Summer of '69", "https://lyrics.lyricfind.com/lyrics/bryan-adams-summer-of-69")] + [InlineData("Patty & the Emblems", "Mixed-Up, Shook-Up Girl", "https://lyrics.lyricfind.com/lyrics/patty-the-emblems-mixed-up-shook-up-girl")] + [InlineData("Mac DeMarco", "Me and Jon, Hanging On", "https://lyrics.lyricfind.com/lyrics/mac-demarco-me-and-jon-hanging-on")] + [InlineData("Of Mice & Men", "You're Not Alone", "https://lyrics.lyricfind.com/lyrics/of-mice-men-youre-not-alone")] + [InlineData("Mac DeMarco", "106.2 Breeze FM", "https://lyrics.lyricfind.com/lyrics/mac-demarco-106-2-breeze-fm")] + [InlineData("Of Mice & Men", "O.C. Loko", "https://lyrics.lyricfind.com/lyrics/of-mice-men-o-c-loko")] + [InlineData("Attack Attack!", "\"I Swear I'll Change\"", "https://lyrics.lyricfind.com/lyrics/attack-attack-i-swear-ill-change")] + [InlineData("The Devil Wears Prada", "You Can't Spell \"Crap\" Without \"C\"", "https://lyrics.lyricfind.com/lyrics/the-devil-wears-prada-you-cant-spell-crap-without-c")] + public void GetLyricUri_MultipleInputs_ShouldBeParse(string artistName, string songName, string expectedUri) + { + // Arrange + var uriConverter = new LyricFindUriConverter(); + + // Act + var actual = uriConverter.GetLyricUri(artistName, songName); + + // Assert + Assert.Equal(new Uri(expectedUri), actual); + } + } +} diff --git a/Tests/LyricsScraperNET.UnitTest/Providers/LyricFind/Resources/Lyrics_HtmlPage_01.txt b/Tests/LyricsScraperNET.UnitTest/Providers/LyricFind/Resources/Lyrics_HtmlPage_01.txt new file mode 100644 index 0000000..5b1d90f --- /dev/null +++ b/Tests/LyricsScraperNET.UnitTest/Providers/LyricFind/Resources/Lyrics_HtmlPage_01.txt @@ -0,0 +1,17 @@ +Lyrics | Parkway Drive | Idols And Anchors
ChartsGenres
Add Song
logoLyricFind
Lyric cover art as blurred background
Lyric cover art

Idols And Anchors

Horizons

Parkway Drive

2007

Apple Music logo
Apple Music logo

Deezer logo
Deezer logo

Spotify logo
Spotify logo
Lyric cover art

Horizons

2007

From This Artist
\ No newline at end of file diff --git a/Tests/LyricsScraperNET.UnitTest/Providers/LyricFind/Resources/Lyrics_Result_01.txt b/Tests/LyricsScraperNET.UnitTest/Providers/LyricFind/Resources/Lyrics_Result_01.txt new file mode 100644 index 0000000..cd67acd --- /dev/null +++ b/Tests/LyricsScraperNET.UnitTest/Providers/LyricFind/Resources/Lyrics_Result_01.txt @@ -0,0 +1,21 @@ +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 when there's nothing left for you to cling to? +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 +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 +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/LyricFind/test_data.json b/Tests/LyricsScraperNET.UnitTest/Providers/LyricFind/test_data.json new file mode 100644 index 0000000..b020041 --- /dev/null +++ b/Tests/LyricsScraperNET.UnitTest/Providers/LyricFind/test_data.json @@ -0,0 +1,9 @@ +[ + { + "LyricPagePath": "Providers/LyricFind/Resources/Lyrics_HtmlPage_01.txt", + "LyricResultPath": "Providers/LyricFind/Resources/Lyrics_Result_01.txt", + "ArtistName": "Parkway Drive", + "SongName": "Idols And Anchors", + "SongUri": null + } +] \ No newline at end of file diff --git a/Tests/LyricsScraperNET.UnitTest/Providers/SongLyrics/SongLyricsUriConverterTests.cs b/Tests/LyricsScraperNET.UnitTest/Providers/SongLyrics/SongLyricsUriConverterTests.cs new file mode 100644 index 0000000..7c0c8b4 --- /dev/null +++ b/Tests/LyricsScraperNET.UnitTest/Providers/SongLyrics/SongLyricsUriConverterTests.cs @@ -0,0 +1,33 @@ +using LyricsScraperNET.Providers.SongLyrics; +using System; +using Xunit; + +namespace LyricsScraperNET.UnitTest.Providers.SongLyrics +{ + public class SongLyricsUriConverterTests + { + [Theory] + [InlineData("Attack Attack!", "I Swear I'll Change", "https://www.songlyrics.com/attack-attack!/i-swear-i-ll-change-lyrics/")] + [InlineData("Attack Attack!", "\"I Swear I'll Change\"", "https://www.songlyrics.com/attack-attack!/i-swear-i-ll-change-lyrics/")] + [InlineData("Against Me!", "Stop!", "https://www.songlyrics.com/against-me!/stop!-lyrics/")] + [InlineData("The Devil Wears Prada", "You Can't Spell Crap Without \"C\"", "https://www.songlyrics.com/the-devil-wears-prada/you-can-t-spell-crap-without-c-lyrics/")] + [InlineData("The Devil Wears Prada", "You Can't Spell \"Crap\" Without \"C\"", "https://www.songlyrics.com/the-devil-wears-prada/you-can-t-spell-crap-without-c-lyrics/")] + [InlineData("Bryan Adams", "Summer of '69", "https://www.songlyrics.com/bryan-adams/summer-of-69-lyrics/")] + [InlineData("Patty & The Emblems", "Mixed Up Shook Up Girl (Long Version)", "https://www.songlyrics.com/patty-the-emblems/mixed-up-shook-up-girl-long-version-lyrics/")] + [InlineData("Mac DeMarco", "Me and Jon, Hanging On", "https://www.songlyrics.com/mac-demarco/me-and-jon-hanging-on-lyrics/")] + [InlineData("Of Mice & Men", "You're Not Alone", "https://www.songlyrics.com/of-mice-men/you-re-not-alone-lyrics/")] + [InlineData("Mac DeMarco", "106.2 Breeze FM", "https://www.songlyrics.com/mac-demarco/106-2-breeze-fm-lyrics/")] + [InlineData("Of Mice & Men", "O.G. Loko", "https://www.songlyrics.com/of-mice-men/o-g-loko-lyrics/")] + public void GetLyricUri_MultipleInputs_ShouldBeParse(string artistName, string songName, string expectedUri) + { + // Arrange + var uriConverter = new SongLyricsUriConverter(); + + // Act + var actual = uriConverter.GetLyricUri(artistName, songName); + + // Assert + Assert.Equal(new Uri(expectedUri), actual); + } + } +} diff --git a/Tests/LyricsScraperNET.UnitTest/Resources/full_test_settings.json b/Tests/LyricsScraperNET.UnitTest/Resources/full_test_settings.json index 166e883..a2a6f75 100644 --- a/Tests/LyricsScraperNET.UnitTest/Resources/full_test_settings.json +++ b/Tests/LyricsScraperNET.UnitTest/Resources/full_test_settings.json @@ -1,21 +1,25 @@ { "LyricScraperClient": { "GeniusOptions": { - "SearchPriority": 2, + "SearchPriority": 11, "Enabled": true, "ApiKey": "" }, "AZLyricsOptions": { - "SearchPriority": 3, + "SearchPriority": 22, "Enabled": true }, "MusixmatchOptions": { - "SearchPriority": 1, + "SearchPriority": 33, "Enabled": true, "ApiKey": "" }, "SongLyricsOptions": { - "SearchPriority": 0, + "SearchPriority": 44, + "Enabled": true + }, + "LyricFindOptions": { + "SearchPriority": 55, "Enabled": true } }