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; } } } }