Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Issue 38: Initial commit for Lyrics Freak Provider #45

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions LyricsScraperNET.Client/appsettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@
"LyricFindOptions": {
"SearchPriority": 1,
"Enabled": true
},
"LyricsFreakOptions": {
"SearchPriority": 5,
"Enabled": true
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,5 +24,6 @@ public interface ILyricScraperClientConfig
IExternalProviderOptions SongLyricsOptions { get; }

IExternalProviderOptions LyricFindOptions { get; }
IExternalProviderOptions LyricsFreakOptions { get; }
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add an empty line before this for better readability

}
}
6 changes: 5 additions & 1 deletion LyricsScraperNET/Configuration/LyricScraperClientConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using LyricsScraperNET.Providers.AZLyrics;
using LyricsScraperNET.Providers.Genius;
using LyricsScraperNET.Providers.LyricFind;
using LyricsScraperNET.Providers.LyricsFreak;
using LyricsScraperNET.Providers.Musixmatch;
using LyricsScraperNET.Providers.SongLyrics;
using System.Text.Json.Serialization;
Expand All @@ -23,6 +24,8 @@ public sealed class LyricScraperClientConfig : ILyricScraperClientConfig

public IExternalProviderOptions LyricFindOptions { get; set; } = new LyricFindOptions();

public IExternalProviderOptions LyricsFreakOptions { get; set; } = new LyricsFreakOptions();

/// <inheritdoc />
public bool UseParallelSearch { get; set; } = false;

Expand All @@ -31,6 +34,7 @@ public sealed class LyricScraperClientConfig : ILyricScraperClientConfig
|| GeniusOptions.Enabled
|| MusixmatchOptions.Enabled
|| SongLyricsOptions.Enabled
|| LyricFindOptions.Enabled;
|| LyricFindOptions.Enabled
|| LyricsFreakOptions.Enabled;
}
}
2 changes: 2 additions & 0 deletions LyricsScraperNET/Configuration/ServiceCollectionExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using LyricsScraperNET.Providers.LyricFind;
using LyricsScraperNET.Providers.Musixmatch;
using LyricsScraperNET.Providers.SongLyrics;
using LyricsScraperNET.Providers.LyricsFreak;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
Expand All @@ -20,6 +21,7 @@ public static IServiceCollection AddLyricScraperClientService(
var lyricScraperClientConfig = configuration.GetSection(LyricScraperClientConfig.ConfigurationSectionName);
if (lyricScraperClientConfig.Exists())
{
services.AddProvider<LyricsFreakOptions, LyricsFreakProvider>(lyricScraperClientConfig);
services.AddProvider<AZLyricsOptions, AZLyricsProvider>(lyricScraperClientConfig);
services.AddProvider<GeniusOptions, GeniusProvider>(lyricScraperClientConfig);
services.AddProvider<SongLyricsOptions, SongLyricsProvider>(lyricScraperClientConfig);
Expand Down
10 changes: 9 additions & 1 deletion LyricsScraperNET/Extensions/LyricsScraperClientExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using LyricsScraperNET.Providers.AZLyrics;
using LyricsScraperNET.Providers.Genius;
using LyricsScraperNET.Providers.LyricFind;
using LyricsScraperNET.Providers.LyricsFreak;
using LyricsScraperNET.Providers.Models;
using LyricsScraperNET.Providers.Musixmatch;
using LyricsScraperNET.Providers.SongLyrics;
Expand Down Expand Up @@ -39,6 +40,12 @@ public static ILyricsScraperClient WithLyricFind(this ILyricsScraperClient lyric
return lyricsScraperClient;
}

public static ILyricsScraperClient WithLyricsFreak(this ILyricsScraperClient lyricsScraperClient)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add a test in the LyricsScraperClientExtensionsTest like: LyricsScraperClient_WithLyricsFreak_ReturnsIsEnabled

{
lyricsScraperClient.AddProvider(new LyricsFreakProvider());
return lyricsScraperClient;
}

/// <summary>
/// Configure LyricsScraperClient with all available providers in <seealso cref="ExternalProviderType"/>.
/// Search lyrics enabled by default for all providers.
Expand All @@ -50,7 +57,8 @@ public static ILyricsScraperClient WithAllProviders(this ILyricsScraperClient ly
.WithAZLyrics()
.WithMusixmatch()
.WithSongLyrics()
.WithLyricFind();
.WithLyricFind()
.WithLyricsFreak();
}
}
}
20 changes: 20 additions & 0 deletions LyricsScraperNET/Extensions/StringExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -71,5 +71,25 @@ public static string СonvertToDashedFormat(this string input, bool useException

return result;
}

public static string СonvertSpaceToPlusFormat(this string input, bool removeProhibitedSymbols = false)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add a couple unit tests in the:
StringExtensionsTest
Take a look at the ases in: СonvertToDashedFormat_MultipleInputs_ShouldBeParse

{
if (string.IsNullOrWhiteSpace(input))
return input;

var result = input.ToLowerInvariant().Trim();

if (removeProhibitedSymbols)
result = new string(result.Where(x => char.IsLetterOrDigit(x) || char.IsWhiteSpace(x) ).ToArray());

result = Regex.Replace(new string(result.Select(x =>
{
return (char.IsWhiteSpace(x))
? '+'
: x;
}).ToArray()), "\\++", "+").Trim('+');

return result;
}
}
}
31 changes: 31 additions & 0 deletions LyricsScraperNET/Providers/LyricsFreak/LyricsFreakOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
using LyricsScraperNET.Providers.Abstract;
using LyricsScraperNET.Providers.Models;

namespace LyricsScraperNET.Providers.LyricsFreak
{
public sealed class LyricsFreakOptions : IExternalProviderOptions
{
public ExternalProviderType ExternalProviderType => ExternalProviderType.LyricsFreak;

public bool Enabled { get; set; }
public int SearchPriority { get; set; } = 5;

public string ConfigurationSectionName { get; } = "LyricsFreakOptions";

public override bool Equals(object? obj)
{
return obj is LyricsFreakOptions options &&
ExternalProviderType == options.ExternalProviderType;
}

public override int GetHashCode()
{
unchecked
{
int hash = 17;
hash = (hash * 31) + ExternalProviderType.GetHashCode();
return hash;
}
}
}
}
21 changes: 21 additions & 0 deletions LyricsScraperNET/Providers/LyricsFreak/LyricsFreakParser.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
using LyricsScraperNET.Providers.Abstract;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Text;
using System.Threading.Tasks;

namespace LyricsScraperNET.Providers.LyricsFreak
{
internal sealed class LyricsFreakParser : IExternalProviderLyricParser
{
public string Parse(string lyric)
{
lyric = WebUtility.HtmlDecode(lyric);

return lyric?.Trim() ?? string.Empty;

}
}
}
150 changes: 150 additions & 0 deletions LyricsScraperNET/Providers/LyricsFreak/LyricsFreakProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
using Genius.Models.Song;
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pleasse sort and remove unused usings

using HtmlAgilityPack;
using LyricsScraperNET.Helpers;
using LyricsScraperNET.Models.Responses;
using LyricsScraperNET.Network;
using LyricsScraperNET.Providers.Abstract;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using System;
using System.Threading;
using System.Threading.Tasks;

namespace LyricsScraperNET.Providers.LyricsFreak
{
internal class LyricsFreakProvider : ExternalProviderBase
{
private ILogger<LyricsFreakProvider>? _logger;
private readonly IExternalUriConverter _uriConverter;
private readonly string LyricsHrefXPath = "//a[translate(@title, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz') = '{0} lyrics']";
private const string LyricsDivXPath = "//div[@data-container-id='lyrics']";

public override IExternalProviderOptions Options { get; }

#region Constructors
public LyricsFreakProvider()
{
Parser = new LyricsFreakParser();
WebClient = new HtmlAgilityWebClient();
Options = new LyricsFreakOptions() { Enabled = true };
_uriConverter = new LyricsFreakUriConverter();
}
public LyricsFreakProvider(ILogger<LyricsFreakProvider> logger, LyricsFreakOptions options)
: this()
{
_logger = logger;
Ensure.ArgumentNotNull(options, nameof(options));
Options = options;
}
public LyricsFreakProvider(ILogger<LyricsFreakProvider> logger, IOptionsSnapshot<LyricsFreakOptions> options)
: this(logger, options.Value)
{
Ensure.ArgumentNotNull(options, nameof(options));
}
public LyricsFreakProvider(LyricsFreakOptions options)
: this(NullLogger<LyricsFreakProvider>.Instance, options)
{
Ensure.ArgumentNotNull(options, nameof(options));
}
public LyricsFreakProvider(IOptionsSnapshot<LyricsFreakOptions> options)
: this(NullLogger<LyricsFreakProvider>.Instance, options.Value)
{
Ensure.ArgumentNotNull(options, nameof(options));
}
#endregion
#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<SearchResult> SearchLyricAsync(string artist, string song, CancellationToken cancellationToken = default)
{
try
{
var artistUri = _uriConverter.GetLyricUri(artist, song);


if (WebClient == null || Parser == null)
{
_logger?.LogWarning($"LyricFind. Please set up WebClient and Parser first");
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is a copy-paste issue in the log messages. For instance, "LyricFind" appears incorrectly here and in other places in this file.

return new SearchResult(Models.ExternalProviderType.LyricsFreak);
}
var htmlResponse = await WebClient.LoadAsync(artistUri, cancellationToken);
cancellationToken.ThrowIfCancellationRequested();

var songUri = ParseForSongUri(htmlResponse, song);

if (string.IsNullOrEmpty(songUri))
{
_logger?.LogWarning($"LyricFind. Can't find song Uri for song: [{song}]");
return new SearchResult(Models.ExternalProviderType.LyricsFreak);
}
var songUriResult = await SearchLyricAsync(new Uri(LyricsFreakUriConverter.BaseUrl + songUri), cancellationToken);
cancellationToken.ThrowIfCancellationRequested();
if (string.IsNullOrEmpty(songUriResult?.LyricText))
{
_logger?.LogWarning($"LyricFind. Can't find song lyrics for song : [{song}]");
return new SearchResult(Models.ExternalProviderType.LyricFind);
}
return new SearchResult(songUriResult.LyricText, Models.ExternalProviderType.LyricsFreak);
}
catch (Exception ex)
{
_logger?.LogError(ex, $"LyricsFreak. Error searching for lyrics for artist: [{artist}], song: [{song}]");
return new SearchResult(Models.ExternalProviderType.LyricsFreak);
}
}
protected async override Task<SearchResult> SearchLyricAsync(Uri uri, CancellationToken cancellationToken = default)
{
var text = await WebClient.LoadAsync(uri, cancellationToken);
cancellationToken.ThrowIfCancellationRequested();

var songHtmlLyrics = ParseForSongLyrics(text);
if (string.IsNullOrEmpty(songHtmlLyrics))
{
_logger?.LogWarning($"LyricFind. Can't find song lyrics for song uri: [{uri.AbsoluteUri}]");
return new SearchResult(Models.ExternalProviderType.LyricFind);
}
var lyricsText = Parser.Parse(songHtmlLyrics);
return new SearchResult(lyricsText, Models.ExternalProviderType.LyricsFreak);
}
#endregion
#region Private methods
private string ParseForSongUri(string htmlBody, string song)
{
var linkNode = GetNode(htmlBody, string.Format(LyricsHrefXPath, song.ToLower()));
if (linkNode == null)
{
return string.Empty;
}

string hrefSong = linkNode.GetAttributeValue("href", string.Empty);
return hrefSong;
}
private string ParseForSongLyrics(string htmlBody)
{
var lyricsNode = GetNode(htmlBody, LyricsDivXPath);
if (lyricsNode == null)
{
return string.Empty;

}
string lyricsText = lyricsNode.InnerText.Trim();
return lyricsText;
}
private HtmlNode? GetNode(string htmlBody, string xPath)
{
var htmlDoc = new HtmlDocument();
htmlDoc.LoadHtml(htmlBody);
return htmlDoc.DocumentNode.SelectSingleNode(xPath);
}
#endregion
}
}
28 changes: 28 additions & 0 deletions LyricsScraperNET/Providers/LyricsFreak/LyricsFreakUriConverter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
using LyricsScraperNET.Extensions;
using LyricsScraperNET.Providers.Abstract;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace LyricsScraperNET.Providers.LyricsFreak
{
internal sealed class LyricsFreakUriConverter : IExternalUriConverter
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Plaese Add Unit Tests for LyricsFreakUriConverter similar to other converters. You can refer to test cases in AZLyricsUriConverterTests. Ensure that the information is correctly fetched from the specified address when writing the tests.

{
public const string BaseUrl = "https://www.lyricsfreak.com";
// 0 - artist, 1 - song
private const string uriArtistPathFormat = BaseUrl + "/{0}/{1}";
public Uri GetLyricUri(string artist, string song)
{
var artistFormatted = artist.ToLowerInvariant().СonvertSpaceToPlusFormat(removeProhibitedSymbols: true);
return GetArtistUri(artistFormatted);
}
// Example for Artist parkway drive https://www.lyricsfreak.com/p/parkway+drive/
private static Uri GetArtistUri(string artist)
{
return new Uri(string.Format(uriArtistPathFormat, artist.Length > 0 ? artist[0] : string.Empty, artist));

}
}
}
3 changes: 2 additions & 1 deletion LyricsScraperNET/Providers/Models/ExternalProviderType.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ public enum ExternalProviderType
Genius,
Musixmatch,
SongLyrics,
LyricFind
LyricFind,
LyricsFreak
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,15 @@
<None Update="Providers\LyricFind\lyric_test_data.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Providers\LyricsFreak\lyric_test_data.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Providers\LyricsFreak\Resources\Lyrics_Result_01.txt">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Providers\LyricsFreak\Resources\Lyrics_Result_02.txt">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Providers\Musixmatch\instrumental_test_data.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
Expand Down
Loading