Skip to content
Merged
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 @@ -26,6 +26,10 @@
"KPopLyricsOptions": {
"SearchPriority": 3,
"Enabled": true
},
"LyricsFreakOptions": {
"SearchPriority": 6,
"Enabled": true
}
}
}
3 changes: 3 additions & 0 deletions LyricsScraperNET/Configuration/ILyricScraperClientConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ public interface ILyricScraperClientConfig
IExternalProviderOptions SongLyricsOptions { get; }

IExternalProviderOptions LyricFindOptions { get; }

IExternalProviderOptions KPopLyricsOptions { get; }

IExternalProviderOptions LyricsFreakOptions { get; }
}
}
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 @@ -26,6 +27,8 @@ public sealed class LyricScraperClientConfig : ILyricScraperClientConfig

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

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

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

Expand All @@ -35,6 +38,7 @@ public sealed class LyricScraperClientConfig : ILyricScraperClientConfig
|| MusixmatchOptions.Enabled
|| SongLyricsOptions.Enabled
|| LyricFindOptions.Enabled
|| KPopLyricsOptions.Enabled;
|| KPopLyricsOptions.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 @@ -5,6 +5,7 @@
using LyricsScraperNET.Providers.Musixmatch;
using LyricsScraperNET.Providers.SongLyrics;
using LyricsScraperNET.Providers.KPopLyrics;
using LyricsScraperNET.Providers.LyricsFreak;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
Expand All @@ -21,6 +22,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
Expand Up @@ -2,6 +2,7 @@
using LyricsScraperNET.Providers.Genius;
using LyricsScraperNET.Providers.KPopLyrics;
using LyricsScraperNET.Providers.LyricFind;
using LyricsScraperNET.Providers.LyricsFreak;
using LyricsScraperNET.Providers.Models;
using LyricsScraperNET.Providers.Musixmatch;
using LyricsScraperNET.Providers.SongLyrics;
Expand Down Expand Up @@ -46,6 +47,12 @@ public static ILyricsScraperClient WithKPopLyrics(this ILyricsScraperClient lyri
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

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done

{
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 @@ -58,7 +65,8 @@ public static ILyricsScraperClient WithAllProviders(this ILyricsScraperClient ly
.WithMusixmatch()
.WithSongLyrics()
.WithLyricFind()
.WithKPopLyrics();
.WithKPopLyrics()
.WithLyricsFreak();
}
}
}
20 changes: 20 additions & 0 deletions LyricsScraperNET/Extensions/StringExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -104,5 +104,25 @@ public static string CreateCombinedUrlSlug(string artist, string songTitle)

return slug.ToLower();
}

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

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done

{
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; } = 6;

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;

}
}
}
156 changes: 156 additions & 0 deletions LyricsScraperNET/Providers/LyricsFreak/LyricsFreakProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
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($"LyricsFreak. Please set up WebClient and Parser first");
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($"LyricsFreak. 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 (songUriResult is null || string.IsNullOrEmpty(songUriResult?.LyricText))
{
_logger?.LogWarning($"LyricsFreak. Can't find song lyrics for song : [{song}]");
return new SearchResult(Models.ExternalProviderType.LyricsFreak);
}
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($"LyricsFreak. Can't find song lyrics for song uri: [{uri.AbsoluteUri}]");
return new SearchResult(Models.ExternalProviderType.LyricsFreak);
}
var lyricsText = Parser.Parse(songHtmlLyrics);
return new SearchResult(lyricsText, Models.ExternalProviderType.LyricsFreak);
}
#endregion
#region Private methods
private string ParseForSongUri(string htmlBody, string song)
{
string formattedXPath = string.Format(LyricsHrefXPath, EncodedSong(song));
var linkNode = GetNode(htmlBody, formattedXPath);
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);
}
private string EncodedSong(string song)
{
string encodedSong = System.Net.WebUtility.HtmlEncode(song).ToLowerInvariant();
encodedSong = encodedSong.Replace("&#39;", "&#039;");
return encodedSong;
}
#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.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done

{
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 @@ -8,6 +8,7 @@ public enum ExternalProviderType
Musixmatch,
SongLyrics,
LyricFind,
KPopLyrics
KPopLyrics,
LyricsFreak
}
}
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ The library currently supports the following providers:
- [MusixMatch](https://www.musixmatch.com/)
- [SongLyrics](https://www.songlyrics.com/)
- [LyricFind](https://www.lyricfind.com/)
- [LyricsFreak](https://www.lyricsfreak.com/) (**Coming soon** 🚧. [Issue #38](https://github.com/skuill/LyricsScraperNET/issues/38))
- [LyricsFreak](https://www.lyricsfreak.com/) (added by [@ajay201402](https://github.com/ajay201402))
- [kpoplyrics](https://www.kpoplyrics.net/) (added by [@Lukeuke](https://github.com/Lukeuke))
- [Letras.mus.br](https://www.letras.mus.br/) (**Coming soon** 🚧. [Issue #40](https://github.com/skuill/LyricsScraperNET/issues/40))
- [darklyrics](http://www.darklyrics.com/) (**Coming soon** 🚧. [Issue #41](https://github.com/skuill/LyricsScraperNET/issues/41))
Expand Down
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