Skip to content

Commit 0071bc4

Browse files
authored
Merge pull request #45 from ajay201402/Issue38
Issue 38: Add LyricsFreak Provider
2 parents a24533e + ad753b4 commit 0071bc4

30 files changed

+3725
-4
lines changed

LyricsScraperNET.Client/appsettings.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@
2626
"KPopLyricsOptions": {
2727
"SearchPriority": 3,
2828
"Enabled": true
29+
},
30+
"LyricsFreakOptions": {
31+
"SearchPriority": 6,
32+
"Enabled": true
2933
}
3034
}
3135
}

LyricsScraperNET/Configuration/ILyricScraperClientConfig.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ public interface ILyricScraperClientConfig
2424
IExternalProviderOptions SongLyricsOptions { get; }
2525

2626
IExternalProviderOptions LyricFindOptions { get; }
27+
2728
IExternalProviderOptions KPopLyricsOptions { get; }
29+
30+
IExternalProviderOptions LyricsFreakOptions { get; }
2831
}
2932
}

LyricsScraperNET/Configuration/LyricScraperClientConfig.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
using LyricsScraperNET.Providers.AZLyrics;
33
using LyricsScraperNET.Providers.Genius;
44
using LyricsScraperNET.Providers.LyricFind;
5+
using LyricsScraperNET.Providers.LyricsFreak;
56
using LyricsScraperNET.Providers.Musixmatch;
67
using LyricsScraperNET.Providers.SongLyrics;
78
using System.Text.Json.Serialization;
@@ -26,6 +27,8 @@ public sealed class LyricScraperClientConfig : ILyricScraperClientConfig
2627

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

30+
public IExternalProviderOptions LyricsFreakOptions { get; set; } = new LyricsFreakOptions();
31+
2932
/// <inheritdoc />
3033
public bool UseParallelSearch { get; set; } = false;
3134

@@ -35,6 +38,7 @@ public sealed class LyricScraperClientConfig : ILyricScraperClientConfig
3538
|| MusixmatchOptions.Enabled
3639
|| SongLyricsOptions.Enabled
3740
|| LyricFindOptions.Enabled
38-
|| KPopLyricsOptions.Enabled;
41+
|| KPopLyricsOptions.Enabled
42+
|| LyricsFreakOptions.Enabled;
3943
}
4044
}

LyricsScraperNET/Configuration/ServiceCollectionExtensions.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using LyricsScraperNET.Providers.Musixmatch;
66
using LyricsScraperNET.Providers.SongLyrics;
77
using LyricsScraperNET.Providers.KPopLyrics;
8+
using LyricsScraperNET.Providers.LyricsFreak;
89
using Microsoft.Extensions.Configuration;
910
using Microsoft.Extensions.DependencyInjection;
1011
using Microsoft.Extensions.Options;
@@ -21,6 +22,7 @@ public static IServiceCollection AddLyricScraperClientService(
2122
var lyricScraperClientConfig = configuration.GetSection(LyricScraperClientConfig.ConfigurationSectionName);
2223
if (lyricScraperClientConfig.Exists())
2324
{
25+
services.AddProvider<LyricsFreakOptions, LyricsFreakProvider>(lyricScraperClientConfig);
2426
services.AddProvider<AZLyricsOptions, AZLyricsProvider>(lyricScraperClientConfig);
2527
services.AddProvider<GeniusOptions, GeniusProvider>(lyricScraperClientConfig);
2628
services.AddProvider<SongLyricsOptions, SongLyricsProvider>(lyricScraperClientConfig);

LyricsScraperNET/Extensions/LyricsScraperClientExtensions.cs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
using LyricsScraperNET.Providers.Genius;
33
using LyricsScraperNET.Providers.KPopLyrics;
44
using LyricsScraperNET.Providers.LyricFind;
5+
using LyricsScraperNET.Providers.LyricsFreak;
56
using LyricsScraperNET.Providers.Models;
67
using LyricsScraperNET.Providers.Musixmatch;
78
using LyricsScraperNET.Providers.SongLyrics;
@@ -46,6 +47,12 @@ public static ILyricsScraperClient WithKPopLyrics(this ILyricsScraperClient lyri
4647
return lyricsScraperClient;
4748
}
4849

50+
public static ILyricsScraperClient WithLyricsFreak(this ILyricsScraperClient lyricsScraperClient)
51+
{
52+
lyricsScraperClient.AddProvider(new LyricsFreakProvider());
53+
return lyricsScraperClient;
54+
}
55+
4956
/// <summary>
5057
/// Configure LyricsScraperClient with all available providers in <seealso cref="ExternalProviderType"/>.
5158
/// Search lyrics enabled by default for all providers.
@@ -58,7 +65,8 @@ public static ILyricsScraperClient WithAllProviders(this ILyricsScraperClient ly
5865
.WithMusixmatch()
5966
.WithSongLyrics()
6067
.WithLyricFind()
61-
.WithKPopLyrics();
68+
.WithKPopLyrics()
69+
.WithLyricsFreak();
6270
}
6371
}
6472
}

LyricsScraperNET/Extensions/StringExtensions.cs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,5 +104,25 @@ public static string CreateCombinedUrlSlug(string artist, string songTitle)
104104

105105
return slug.ToLower();
106106
}
107+
108+
public static string СonvertSpaceToPlusFormat(this string input, bool removeProhibitedSymbols = false)
109+
{
110+
if (string.IsNullOrWhiteSpace(input))
111+
return input;
112+
113+
var result = input.ToLowerInvariant().Trim();
114+
115+
if (removeProhibitedSymbols)
116+
result = new string(result.Where(x => char.IsLetterOrDigit(x) || char.IsWhiteSpace(x) ).ToArray());
117+
118+
result = Regex.Replace(new string(result.Select(x =>
119+
{
120+
return (char.IsWhiteSpace(x))
121+
? '+'
122+
: x;
123+
}).ToArray()), "\\++", "+").Trim('+');
124+
125+
return result;
126+
}
107127
}
108128
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
using LyricsScraperNET.Providers.Abstract;
2+
using LyricsScraperNET.Providers.Models;
3+
4+
namespace LyricsScraperNET.Providers.LyricsFreak
5+
{
6+
public sealed class LyricsFreakOptions : IExternalProviderOptions
7+
{
8+
public ExternalProviderType ExternalProviderType => ExternalProviderType.LyricsFreak;
9+
10+
public bool Enabled { get; set; }
11+
public int SearchPriority { get; set; } = 6;
12+
13+
public string ConfigurationSectionName { get; } = "LyricsFreakOptions";
14+
15+
public override bool Equals(object? obj)
16+
{
17+
return obj is LyricsFreakOptions options &&
18+
ExternalProviderType == options.ExternalProviderType;
19+
}
20+
21+
public override int GetHashCode()
22+
{
23+
unchecked
24+
{
25+
int hash = 17;
26+
hash = (hash * 31) + ExternalProviderType.GetHashCode();
27+
return hash;
28+
}
29+
}
30+
}
31+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
using LyricsScraperNET.Providers.Abstract;
2+
using System;
3+
using System.Collections.Generic;
4+
using System.Linq;
5+
using System.Net;
6+
using System.Text;
7+
using System.Threading.Tasks;
8+
9+
namespace LyricsScraperNET.Providers.LyricsFreak
10+
{
11+
internal sealed class LyricsFreakParser : IExternalProviderLyricParser
12+
{
13+
public string Parse(string lyric)
14+
{
15+
lyric = WebUtility.HtmlDecode(lyric);
16+
17+
return lyric?.Trim() ?? string.Empty;
18+
19+
}
20+
}
21+
}
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
using HtmlAgilityPack;
2+
using LyricsScraperNET.Helpers;
3+
using LyricsScraperNET.Models.Responses;
4+
using LyricsScraperNET.Network;
5+
using LyricsScraperNET.Providers.Abstract;
6+
using Microsoft.Extensions.Logging;
7+
using Microsoft.Extensions.Logging.Abstractions;
8+
using Microsoft.Extensions.Options;
9+
using System;
10+
using System.Threading;
11+
using System.Threading.Tasks;
12+
13+
namespace LyricsScraperNET.Providers.LyricsFreak
14+
{
15+
internal class LyricsFreakProvider : ExternalProviderBase
16+
{
17+
private ILogger<LyricsFreakProvider>? _logger;
18+
private readonly IExternalUriConverter _uriConverter;
19+
private readonly string LyricsHrefXPath = "//a[translate(@title, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz') = '{0} lyrics']";
20+
private const string LyricsDivXPath = "//div[@data-container-id='lyrics']";
21+
22+
public override IExternalProviderOptions Options { get; }
23+
24+
#region Constructors
25+
public LyricsFreakProvider()
26+
{
27+
Parser = new LyricsFreakParser();
28+
WebClient = new HtmlAgilityWebClient();
29+
Options = new LyricsFreakOptions() { Enabled = true };
30+
_uriConverter = new LyricsFreakUriConverter();
31+
}
32+
public LyricsFreakProvider(ILogger<LyricsFreakProvider> logger, LyricsFreakOptions options)
33+
: this()
34+
{
35+
_logger = logger;
36+
Ensure.ArgumentNotNull(options, nameof(options));
37+
Options = options;
38+
}
39+
public LyricsFreakProvider(ILogger<LyricsFreakProvider> logger, IOptionsSnapshot<LyricsFreakOptions> options)
40+
: this(logger, options.Value)
41+
{
42+
Ensure.ArgumentNotNull(options, nameof(options));
43+
}
44+
public LyricsFreakProvider(LyricsFreakOptions options)
45+
: this(NullLogger<LyricsFreakProvider>.Instance, options)
46+
{
47+
Ensure.ArgumentNotNull(options, nameof(options));
48+
}
49+
public LyricsFreakProvider(IOptionsSnapshot<LyricsFreakOptions> options)
50+
: this(NullLogger<LyricsFreakProvider>.Instance, options.Value)
51+
{
52+
Ensure.ArgumentNotNull(options, nameof(options));
53+
}
54+
#endregion
55+
#region Sync
56+
protected override SearchResult SearchLyric(string artist, string song, CancellationToken cancellationToken = default)
57+
{
58+
return SearchLyricAsync(artist, song, cancellationToken).GetAwaiter().GetResult();
59+
}
60+
protected override SearchResult SearchLyric(Uri uri, CancellationToken cancellationToken = default)
61+
{
62+
return SearchLyricAsync(uri, cancellationToken).GetAwaiter().GetResult();
63+
}
64+
#endregion
65+
#region Async
66+
protected override async Task<SearchResult> SearchLyricAsync(string artist, string song, CancellationToken cancellationToken = default)
67+
{
68+
try
69+
{
70+
var artistUri = _uriConverter.GetLyricUri(artist, song);
71+
72+
73+
if (WebClient == null || Parser == null)
74+
{
75+
_logger?.LogWarning($"LyricsFreak. Please set up WebClient and Parser first");
76+
return new SearchResult(Models.ExternalProviderType.LyricsFreak);
77+
}
78+
var htmlResponse = await WebClient.LoadAsync(artistUri, cancellationToken);
79+
cancellationToken.ThrowIfCancellationRequested();
80+
81+
var songUri = ParseForSongUri(htmlResponse, song);
82+
83+
if (string.IsNullOrEmpty(songUri))
84+
{
85+
_logger?.LogWarning($"LyricsFreak. Can't find song Uri for song: [{song}]");
86+
return new SearchResult(Models.ExternalProviderType.LyricsFreak);
87+
}
88+
var songUriResult = await SearchLyricAsync(new Uri(LyricsFreakUriConverter.BaseUrl + songUri), cancellationToken);
89+
cancellationToken.ThrowIfCancellationRequested();
90+
if (songUriResult is null || string.IsNullOrEmpty(songUriResult?.LyricText))
91+
{
92+
_logger?.LogWarning($"LyricsFreak. Can't find song lyrics for song : [{song}]");
93+
return new SearchResult(Models.ExternalProviderType.LyricsFreak);
94+
}
95+
return new SearchResult(songUriResult!.LyricText, Models.ExternalProviderType.LyricsFreak);
96+
}
97+
catch (Exception ex)
98+
{
99+
_logger?.LogError(ex, $"LyricsFreak. Error searching for lyrics for artist: [{artist}], song: [{song}]");
100+
return new SearchResult(Models.ExternalProviderType.LyricsFreak);
101+
}
102+
}
103+
protected async override Task<SearchResult> SearchLyricAsync(Uri uri, CancellationToken cancellationToken = default)
104+
{
105+
var text = await WebClient.LoadAsync(uri, cancellationToken);
106+
cancellationToken.ThrowIfCancellationRequested();
107+
108+
var songHtmlLyrics = ParseForSongLyrics(text);
109+
if (string.IsNullOrEmpty(songHtmlLyrics))
110+
{
111+
_logger?.LogWarning($"LyricsFreak. Can't find song lyrics for song uri: [{uri.AbsoluteUri}]");
112+
return new SearchResult(Models.ExternalProviderType.LyricsFreak);
113+
}
114+
var lyricsText = Parser.Parse(songHtmlLyrics);
115+
return new SearchResult(lyricsText, Models.ExternalProviderType.LyricsFreak);
116+
}
117+
#endregion
118+
#region Private methods
119+
private string ParseForSongUri(string htmlBody, string song)
120+
{
121+
string formattedXPath = string.Format(LyricsHrefXPath, EncodedSong(song));
122+
var linkNode = GetNode(htmlBody, formattedXPath);
123+
if (linkNode == null)
124+
{
125+
return string.Empty;
126+
}
127+
128+
string hrefSong = linkNode.GetAttributeValue("href", string.Empty);
129+
return hrefSong;
130+
}
131+
private string ParseForSongLyrics(string htmlBody)
132+
{
133+
var lyricsNode = GetNode(htmlBody, LyricsDivXPath);
134+
if (lyricsNode == null)
135+
{
136+
return string.Empty;
137+
138+
}
139+
string lyricsText = lyricsNode.InnerText.Trim();
140+
return lyricsText;
141+
}
142+
private HtmlNode? GetNode(string htmlBody, string xPath)
143+
{
144+
var htmlDoc = new HtmlDocument();
145+
htmlDoc.LoadHtml(htmlBody);
146+
return htmlDoc.DocumentNode.SelectSingleNode(xPath);
147+
}
148+
private string EncodedSong(string song)
149+
{
150+
string encodedSong = System.Net.WebUtility.HtmlEncode(song).ToLowerInvariant();
151+
encodedSong = encodedSong.Replace("&#39;", "&#039;");
152+
return encodedSong;
153+
}
154+
#endregion
155+
}
156+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
using LyricsScraperNET.Extensions;
2+
using LyricsScraperNET.Providers.Abstract;
3+
using System;
4+
using System.Collections.Generic;
5+
using System.Linq;
6+
using System.Text;
7+
using System.Threading.Tasks;
8+
9+
namespace LyricsScraperNET.Providers.LyricsFreak
10+
{
11+
internal sealed class LyricsFreakUriConverter : IExternalUriConverter
12+
{
13+
public const string BaseUrl = "https://www.lyricsfreak.com";
14+
// 0 - artist, 1 - song
15+
private const string uriArtistPathFormat = BaseUrl + "/{0}/{1}";
16+
public Uri GetLyricUri(string artist, string song)
17+
{
18+
var artistFormatted = artist.ToLowerInvariant().СonvertSpaceToPlusFormat(removeProhibitedSymbols: true);
19+
return GetArtistUri(artistFormatted);
20+
}
21+
// Example for Artist parkway drive https://www.lyricsfreak.com/p/parkway+drive/
22+
private static Uri GetArtistUri(string artist)
23+
{
24+
return new Uri(string.Format(uriArtistPathFormat, artist.Length > 0 ? artist[0] : string.Empty, artist));
25+
26+
}
27+
}
28+
}

0 commit comments

Comments
 (0)