1+ using GeniusAPI . Internal . Models ;
2+ using GeniusAPI . Models ;
3+ using HtmlAgilityPack ;
4+ using Microsoft . Extensions . Logging ;
5+ using System . Net ;
6+ using System . Text ;
7+ using System . Text . Json ;
8+
9+ namespace GeniusAPI ;
10+
11+ /// <summary>
12+ /// Allows to search and fetch track lyrics on Genius.
13+ /// </summary>
14+ public class LyricsClient
15+ {
16+ readonly ILogger < LyricsClient > ? logger = null ;
17+
18+ readonly HttpClient client = new ( ) ;
19+
20+ /// <summary>
21+ /// Creates a new LyricsClient
22+ /// </summary>
23+ /// <param name="accessToken">The access token for the Genius API. Get one for free here: https://genius.com/api-clients</param>
24+ public LyricsClient (
25+ string accessToken )
26+ {
27+ client . DefaultRequestHeaders . Authorization = new ( "Bearer" , accessToken ) ;
28+ }
29+
30+ /// <summary>
31+ /// Creates a new LyricsClient
32+ /// </summary>
33+ /// <param name="accessToken">The access token for the Genius API. Get one for free here: https://genius.com/api-clients</param>
34+ /// <param name="logger">The logger used to log.</param>
35+ public LyricsClient (
36+ string accessToken ,
37+ ILogger < LyricsClient > logger )
38+ {
39+ this . logger = logger ;
40+
41+ client . DefaultRequestHeaders . Authorization = new ( "Bearer" , accessToken ) ;
42+ }
43+
44+
45+ /// <summary>
46+ /// Searches for tracks on Genius.
47+ /// </summary>
48+ /// <param name="query">The query to search for.</param>
49+ /// <param name="cancellationToken">The token to cancel this action.</param>
50+ /// <returns>An array containing all found tracks.</returns>
51+ public async Task < IEnumerable < LyricsTrack > > SearchTracksAsync (
52+ string query ,
53+ CancellationToken cancellationToken = default )
54+ {
55+ logger ? . LogInformation ( "[LyricsClient-SearchTracksAsync] Searching for tracks on Genius..." ) ;
56+ HttpResponseMessage response = await client . GetAsync ( $ "https://api.genius.com/search?q={ WebUtility . UrlEncode ( query ) } ", cancellationToken ) ;
57+ response . EnsureSuccessStatusCode ( ) ;
58+
59+ logger ? . LogInformation ( "[LyricsClient-SearchTracksAsync] Parsing search results..." ) ;
60+ string body = await response . Content . ReadAsStringAsync ( cancellationToken ) ;
61+
62+ LyricsRequestResult ? result = JsonSerializer . Deserialize < LyricsRequestResult > ( body ) ;
63+ if ( result is null )
64+ {
65+ logger ? . LogError ( "[LyricsClient-SearchTracksAsync] Failed to search for tracks on Genius: Parsed search result is null" ) ;
66+ throw new NullReferenceException ( "Failed to search for tracks on Genius." ) ;
67+ }
68+ if ( result . Meta . StatusCode != 200 )
69+ {
70+ logger ? . LogError ( "[LyricsClient-SearchTracksAsync] Failed to search for lyrics: {statusCode}, {message}" , result . Meta . StatusCode , result . Meta . Message ) ;
71+ throw new Exception ( $ "Lyrics request did not return successful status code: { result . Meta . StatusCode } .", new ( result . Meta . Message ?? "Unknown message." ) ) ;
72+ }
73+
74+ return result . Response . Hits
75+ . Where ( hit => hit . Type == "song" )
76+ . Select ( hit => hit . Track ) ;
77+ }
78+
79+
80+ static void ExtractText (
81+ HtmlNode node ,
82+ StringBuilder builder )
83+ {
84+ foreach ( HtmlNode childNode in node . ChildNodes )
85+ switch ( childNode . NodeType )
86+ {
87+ case HtmlNodeType . Text :
88+ builder . Append ( childNode . InnerText ) ;
89+ break ;
90+ case HtmlNodeType . Element :
91+ if ( childNode . Name == "br" )
92+ builder . AppendLine ( ) ;
93+ else
94+ ExtractText ( childNode , builder ) ;
95+ break ;
96+ }
97+ }
98+
99+ /// <summary>
100+ /// Fetches the lyrics of a track on Genius.
101+ /// </summary>
102+ /// <param name="url">The url of the track to fetch the lyrics for.</param>
103+ /// <param name="cancellationToken">The token to cancel this action.</param>
104+ /// <returns>A string representing the lyrics.</returns>
105+ public async Task < string > FetchLyricsAsync (
106+ string url ,
107+ CancellationToken cancellationToken = default )
108+ {
109+ logger ? . LogInformation ( "[LyricsClient-FetchLyricsAsync] Fetching track lyrics..." ) ;
110+ HttpResponseMessage response = await client . GetAsync ( url , cancellationToken ) ;
111+ response . EnsureSuccessStatusCode ( ) ;
112+
113+ logger ? . LogInformation ( "[LyricsClient-FetchLyricsAsync] Parsing track lyrics..." ) ;
114+ HtmlDocument html = new ( ) ;
115+ html . Load ( await response . Content . ReadAsStreamAsync ( cancellationToken ) ) ;
116+
117+ HtmlNodeCollection nodes = html . DocumentNode . SelectNodes ( "//div[@data-lyrics-container]" ) ;
118+ if ( nodes is null || nodes . Count == 0 )
119+ {
120+ logger ? . LogError ( "[LyricsClient-FetchLyricsAsync] Failed to fetch track lyrics: Parsed HTML nodes is null or empty" ) ;
121+ throw new NullReferenceException ( "Failed to fetch track lyrics." ) ;
122+ }
123+
124+ StringBuilder builder = new ( ) ;
125+ foreach ( HtmlNode node in nodes )
126+ {
127+ ExtractText ( node , builder ) ;
128+ builder . AppendLine ( ) ;
129+ }
130+
131+ return WebUtility . HtmlDecode ( builder . ToString ( ) . TrimEnd ( ) ) ;
132+ }
133+ }
0 commit comments