using AngleSharp;
using AngleSharp.Parser.Html;
using NLog;
using System;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
namespace Sony.Filtr.SpotifyWebAPI
{
public class InternalSpotifyApiClient
{
private readonly HttpClient _apiRequestClient = new HttpClient();
private readonly Logger _logger;
private DateTime? _accessTokenExpiresOn;
private bool TokenIsExpired => _accessTokenExpiresOn <= DateTime.UtcNow;
public InternalSpotifyApiClient()
{ }
public InternalSpotifyApiClient(Logger logger)
{
_logger = logger;
}
public async Task GetArtistInfoAsync(string artistId)
{
if (TokenIsExpired)
await GetScrapedTokenAsync();
try
{
return await FetchArtistInfoAsync(artistId);
}
catch (UnauthorizedAccessException e)
{
_logger?.Error(e, "Unauthorized request to internal API - invalidating token.");
_accessTokenExpiresOn = DateTime.UtcNow;
await GetScrapedTokenAsync();
return await FetchArtistInfoAsync(artistId);
}
}
private async Task FetchArtistInfoAsync(string artistId)
{
var artistInfoUrl = new Flurl.Url("https://spclient.wg.spotify.com/open-backend-2/v1/artists/").AppendPathSegment(artistId);
var response = await _apiRequestClient.GetAsync(artistInfoUrl);
if (response.StatusCode == HttpStatusCode.Unauthorized)
throw new UnauthorizedAccessException();
response.EnsureSuccessStatusCode();
return await response.Content.ReadAsAsync();
}
private async Task GetScrapedTokenAsync()
{
_logger?.Info("Scraping access token");
using (var httpClient = new HttpClient())
{
//try get latest version of chrome as the server blacklists old version numbers
var chromeVersionNumber = await GetChromeVersionNumber(httpClient);
// User agent needs to be browser to force correct rendering, including "Set-Cookie" header with token.
httpClient.DefaultRequestHeaders.UserAgent.ParseAdd($"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/{chromeVersionNumber} Safari/537.36");
// TODO: Just a random artist ID used here, should use something safer.
var response = await httpClient.GetAsync("https://open.spotify.com/artist/3jOstUTkEu2JkjvRdBA5Gu");
if (response.Headers.TryGetValues("Set-Cookie", out var cookieHeaderValues))
{
var tokenValue = cookieHeaderValues.FirstOrDefault(v => v.StartsWith("wp_access_token"));
if (!string.IsNullOrWhiteSpace(tokenValue))
{
var token = tokenValue.Substring(tokenValue.IndexOf("wp_access_token=", StringComparison.Ordinal) + "wp_access_token=".Length).Split(';').FirstOrDefault();
Console.WriteLine("Token: " + token);
_apiRequestClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
//the access token expires 3300 seconds after creation
//set an expiry time to know when and avoid making unauthorized requests
_accessTokenExpiresOn = DateTime.UtcNow.AddSeconds(3295);
_logger?.Info($"New token set to: { token }");
}
}
}
}
private async Task GetChromeVersionNumber(HttpClient httpClient)
{
var wikipediaVersionHtml = await httpClient.GetStringAsync("https://en.wikipedia.org/wiki/Google_Chrome_version_history");
var htmlDocument = new HtmlParser(new Configuration().WithCss()).Parse(wikipediaVersionHtml);
var innerHtmlFromTdWithVersionNumber = htmlDocument.All.FirstOrDefault(x =>
x.LocalName == "td"
&& x.OuterHtml.Contains("a0e75a")
&& x.InnerHtml.Any(y => char.IsDigit(y))
)?.InnerHtml;
var chromeVersionNumber = "72.0.3578.98";
if (!string.IsNullOrEmpty(innerHtmlFromTdWithVersionNumber))
{
chromeVersionNumber = Regex.Replace(innerHtmlFromTdWithVersionNumber, @"\t|\n|\r", "");
_logger?.Info($"Scraped Chrome version number: {chromeVersionNumber}");
}
else
{
_logger?.Warn($"Could not scrape Chrome version number! Using default {chromeVersionNumber}");
}
return chromeVersionNumber;
}
public class ArtistInfoRootObject
{
public string bio { get; set; }
public Headerimage[] headerImages { get; set; }
public Artistinsights artistInsights { get; set; }
}
public class Artistinsights
{
public string artist_gid { get; set; }
public Autobiography autobiography { get; set; }
public string biography { get; set; }
public Header_Image header_image { get; set; }
public Image[] images { get; set; }
public int global_chart_position { get; set; }
public long monthly_listeners { get; set; }
public long monthly_listeners_delta { get; set; }
public int follower_count { get; set; }
public int following_count { get; set; }
public Playlists playlists { get; set; }
public City[] cities { get; set; }
}
public class Autobiography
{
public string[] urls { get; set; }
public Links links { get; set; }
}
public class Links
{
public string twitter { get; set; }
public string instagram { get; set; }
public string wikipedia { get; set; }
public string facebook { get; set; }
}
public class Header_Image
{
public string id { get; set; }
public string uri { get; set; }
public int width { get; set; }
public int height { get; set; }
}
public class Playlists
{
public Entry[] entries { get; set; }
}
public class Entry
{
public string uri { get; set; }
public string name { get; set; }
public string image_url { get; set; }
public Owner owner { get; set; }
public int listeners { get; set; }
}
public class Owner
{
public string name { get; set; }
public string uri { get; set; }
}
public class Image
{
public string id { get; set; }
public string uri { get; set; }
public int width { get; set; }
public int height { get; set; }
}
public class City
{
public string country { get; set; }
public string region { get; set; }
public string city { get; set; }
public int listeners { get; set; }
}
public class Headerimage
{
public string url { get; set; }
}
}
}