using Flurl; using Newtonsoft.Json; using Sony.Filtr.AppleMusic.Models; using System; using System.Collections.Generic; using System.Linq; using System.Net; using System.Net.Http; using System.Threading.Tasks; using Sony.Filtr.AppleMusic.Constants; using Amazon.S3; using Amazon.S3.Transfer; using Sony.Filtr.Core.ResponseCollector; namespace Sony.Filtr.AppleMusic { public class AppleMusicApi { private readonly AppleMusicTokenGenerator _tokenGenerator; private readonly HttpClient _client; private readonly ResponseCollector _responseCollector; public AppleMusicApi(AppleMusicTokenGenerator tokenGenerator, ResponseCollector responseCollector) { _tokenGenerator = tokenGenerator; _responseCollector = responseCollector; _client = BuildApiHttpClient(); } public async Task GetSongChartAsync(string storeFront = null, string genreId = null) { var url = $"/v1/catalog/{storeFront}/charts?types=songs&genre={genreId}&limit=100"; var response = await _client.GetStringAsync(url); var chart = JsonConvert.DeserializeObject(response); var songs = chart?.results?.songs?.FirstOrDefault(); var nextUrl = songs?.next; while (!string.IsNullOrWhiteSpace(nextUrl)) { response = await _client.GetStringAsync(nextUrl); chart = JsonConvert.DeserializeObject(response); var moreSongs = chart?.results?.songs?.FirstOrDefault(); if (moreSongs != null) { songs.data.AddRange(moreSongs.data); nextUrl = moreSongs.next; } else { nextUrl = null; } } return songs; } public async Task GetFullChartAsync(string storeFront, long? genreId = null, int? limit = 50) { var initialResponse = await GetChartAsync(storeFront, chartType: null, genreId: genreId, limit: limit); var viewModel = new FullChartViewModel(); if (initialResponse.results.musicVideos != null) { foreach (var resultsMusicVideos in initialResponse.results.musicVideos.Where(p => p.data != null)) { List musicVideos = new List(); musicVideos.AddRange(resultsMusicVideos.data); string nextUrl = resultsMusicVideos.next; while (nextUrl != null) { var moreData = await GetChartResponseAsync(nextUrl); if (moreData?.results.musicVideos?.FirstOrDefault()?.data != null) { var chartData = moreData.results.musicVideos.First(); //The next url should only specify this chart type. musicVideos.AddRange(chartData.data); nextUrl = chartData.next; } else { nextUrl = null; } } viewModel.MusicVideos.Add(resultsMusicVideos.chart, musicVideos); } } if (initialResponse.results.songs != null) { foreach (var resultsSong in initialResponse.results.songs.Where(p => p.data != null)) { List songs = new List(); songs.AddRange(resultsSong.data); string nextUrl = resultsSong.next; while (nextUrl != null) { var moreData = await GetChartResponseAsync(nextUrl); if (moreData?.results.songs?.FirstOrDefault()?.data != null) { var chartData = moreData.results.songs.First(); //The next url should only specify this chart type. songs.AddRange(chartData.data); nextUrl = chartData.next; } else { nextUrl = null; } } viewModel.Songs.Add(resultsSong.chart, songs); } } if (initialResponse.results.albums != null) { foreach (var resultsAlbum in initialResponse.results.albums.Where(p => p.data != null)) { List albums = new List(); albums.AddRange(resultsAlbum.data); string nextUrl = resultsAlbum.next; while (nextUrl != null) { var moreData = await GetChartResponseAsync(nextUrl); if (moreData?.results.albums?.FirstOrDefault()?.data != null) { var chartData = moreData.results.albums.First(); //The next url should only specify this chart type. albums.AddRange(chartData.data); nextUrl = chartData.next; } else { nextUrl = null; } } viewModel.Albums.Add(resultsAlbum.chart, albums); } } if (initialResponse.results.playlists != null) { foreach (var resultsPlaylist in initialResponse.results.playlists.Where(p => p.data != null)) { List playlists = new List(); playlists.AddRange(resultsPlaylist.data); string nextUrl = resultsPlaylist.next; while (nextUrl != null) { var moreData = await GetChartResponseAsync(nextUrl); if (moreData?.results.albums?.FirstOrDefault()?.data != null) { var chartData = moreData.results.playlists.First(); //The next url should only specify this chart type. playlists.AddRange(chartData.data); nextUrl = chartData.next; } else { nextUrl = null; } } viewModel.Playlists.Add(resultsPlaylist.chart, playlists); } } return viewModel; } public async Task GetChartAsync(string storeFront, string chartType = null, long? genreId = null, int? limit = null) { if (string.IsNullOrWhiteSpace(chartType)) { chartType = "albums,songs,music-videos,playlists"; } var url = new Flurl.Url($"/v1/catalog/{storeFront}/charts?types={chartType}"); if (genreId != null) { url.SetQueryParam("genre", genreId); } if (limit.HasValue) { url.SetQueryParam("limit", limit.Value); } var chart = await GetChartResponseAsync(url); return chart; } public async Task> GetPaginatedSongChartAsync(string storeFront, string chartname, long? genreId = null, int? limit = 50) { var url = new Flurl.Url($"/v1/catalog/{storeFront}/charts?types=songs"); if (genreId != null) { url.SetQueryParam("genre", genreId); } if (limit.HasValue) { url.SetQueryParam("limit", limit.Value); } List songData = new List(); var chart = await GetChartResponseAsync(url); var songs = chart.results.songs.SingleOrDefault(p => p.chart == chartname); if (songs != null && songs.data != null) { songData.AddRange(songs.data); string nextUrl = songs.next; while (nextUrl != null) { var nextUrlToUse = new Flurl.Url(nextUrl); nextUrlToUse.SetQueryParam("limit", limit); //Force limit for requests since apple music defaults to lower number. var moreData = await GetChartResponseAsync(nextUrlToUse); var moreSongs = moreData?.results?.songs?.SingleOrDefault(p => p.chart == chartname); if (moreSongs?.data != null) { songData.AddRange(moreSongs.data); nextUrl = moreSongs.next; } else { nextUrl = null; } } } return songData; } private async Task GetChartResponseAsync(Url url) { var response = await _client.GetAsync(url); if (!response.IsSuccessStatusCode) { /* https://api.music.apple.com/v1/catalog/ag/charts?chart=most-played&genre=15&limit=50&offset=206&types=albums { "errors": [ { "id": "NVCX563G726IXQBZ6GVUK4FJSI", "title": "Invalid Parameter Value", "detail": "offset must be less than 200", "status": "400", "code": "40005", "source": { "parameter": "offset" } } ] } https://developer.apple.com/forums/thread/656390#656390021 */ string body = await response.Content.ReadAsStringAsync(); throw new HttpRequestException($"Could not request Apple Music Chart. URL: '{url}'. StatusCode: {response.StatusCode}. Body: '{body}'"); } var responseString = await response.Content.ReadAsStringAsync(); _responseCollector.PrepareFile(url, "FetchAppleMusicCharts", responseString); var chart = JsonConvert.DeserializeObject(responseString); return chart; } public async Task> GetChartGenresAsync(string storeFront) { var response = await _client.GetStringAsync($"/v1/catalog/{storeFront.ToLowerInvariant()}/genres"); var genresResponse = JsonConvert.DeserializeObject(response); return genresResponse.data; } public async Task GetAllStorefrontsAsync() { var url = $"/v1/storefronts"; var response = await _client.GetStringAsync(url); var storefronts = JsonConvert.DeserializeObject(response); return storefronts; } public async Task> GetPlaylistsAsync(string storeFront, IEnumerable playlistIds) { var url = $"/v1/catalog/{storeFront}/playlists?ids={string.Join(",", playlistIds)}"; var response = await _client.GetStringAsync(url); var playlistResponse = JsonConvert.DeserializeObject(response); return playlistResponse?.data; } public async Task> GetPlaylistsWithFullTracklistsAsync(string storefront, IEnumerable playlistIds) { if (!playlistIds.Any()) { return new List(); } var url = $"/v1/catalog/{storefront}/playlists?ids={string.Join(",", playlistIds)}"; var response = await _client.GetStringAsync(url); var playlistResponse = JsonConvert.DeserializeObject(response); //TODO figure out if we need pagination some other way as music videos do not seem to be included (at least on {playlist}/tracks/) 2018-11-27 if (playlistResponse?.data != null && playlistResponse.data.Any(x => x.relationships?.tracks?.data?.Count >= AppleMusicApiConstants.TrackDefaultPaginationLimit)) { foreach (var playlistData in playlistResponse.data.Where(x => x?.relationships?.tracks?.data.Count >= AppleMusicApiConstants.TrackDefaultPaginationLimit)) { playlistData.relationships.tracks.data = await GetPlaylistTracksAsync(storefront, playlistData.id); } } return playlistResponse?.data; } public async Task> GetPlaylistTracksAsync(string storefront, string playlistId) { var tracks = new List(); var url = $"/v1/catalog/{storefront}/playlists/{playlistId}/tracks?limit={AppleMusicApiConstants.TrackMaxPaginationLimit}"; do { var response = await _client.GetStringAsync(url); var tracksResponse = JsonConvert.DeserializeObject(response); tracks.AddRange(tracksResponse.Data); string nextUrl = null; if (tracksResponse.Next != null) nextUrl = new Flurl.Url(tracksResponse.Next).SetQueryParam("limit", AppleMusicApiConstants.TrackMaxPaginationLimit); url = nextUrl; } while (url != null); return tracks; } public async Task GetPlaylistAsync(string storeFront, string playlistId, string jobName = null) { var url = $"/v1/catalog/{storeFront}/playlists/{playlistId}"; var response = await _client.GetAsync(url); if (!response.IsSuccessStatusCode) { return null; } var responseString = await response.Content.ReadAsStringAsync(); if (!string.IsNullOrEmpty(jobName)) { _responseCollector.PrepareFile(url, jobName, responseString); } var playlistResponse = JsonConvert.DeserializeObject(responseString); return playlistResponse?.data?.FirstOrDefault(); } public async Task GetPlaylistWithFullTracklistAsync(string storefront, string playlistId) { var url = $"/v1/catalog/{storefront}/playlists/{playlistId}"; var response = await _client.GetStringAsync(url); var playlistResponse = JsonConvert.DeserializeObject(response); var playlist = playlistResponse?.data?.FirstOrDefault(); //TODO figure out if we need pagination some other way as music videos do not seem to be included (at least on {playlist}/tracks/) 2018-11-27 if (playlist?.relationships?.tracks?.data?.Count >= AppleMusicApiConstants.TrackDefaultPaginationLimit) playlist.relationships.tracks.data = await GetPlaylistTracksAsync(storefront, playlistId); return playlist; } public async Task> GetCuratorsAsync(string storeFrontId, IEnumerable curatorIds) { var url = new Url($"/v1/catalog/{storeFrontId}/curators"); url.SetQueryParam("ids", string.Join(",", curatorIds)); var response = await _client.GetStringAsync(url); var curatorResponse = JsonConvert.DeserializeObject(response); return curatorResponse?.data; } private HttpClient BuildApiHttpClient() { var baseHandler = new HttpClientHandler() { UseCookies = false, AutomaticDecompression = DecompressionMethods.Deflate | DecompressionMethods.GZip }; var authHandler = new AppleMusicAuthClientCredentialsHttpMessageHandler(_tokenGenerator, baseHandler); return new HttpClient(authHandler) { BaseAddress = new Uri(AppleMusicApiConstants.ApiUrlBase) }; } public async Task> GetSongsAsync(string storefrontId, IEnumerable songIds) { var url = new Flurl.Url($"/v1/catalog/{storefrontId}/songs"); url.SetQueryParam("ids", string.Join(",", songIds)); var response = await _client.GetStringAsync(url); var songResponse = JsonConvert.DeserializeObject(response); return songResponse.data; } public async Task> GetAlbumsAsync(string storefrontId, IEnumerable albumIds) { var url = new Url($"/v1/catalog/{storefrontId}/albums"); url.SetQueryParam("ids", string.Join(",", albumIds)); var response = await _client.GetStringAsync(url); var albumResponse = JsonConvert.DeserializeObject(response); return albumResponse.data; } public async Task> GetArtistsAsync(string storefrontId, IEnumerable artistIds) { var url = new Flurl.Url($"/v1/catalog/{storefrontId}/artists"); url.SetQueryParam("ids", string.Join(",", artistIds)); var response = await _client.GetStringAsync(url); var songResponse = JsonConvert.DeserializeObject(response); return songResponse.data; } } public class FullChartViewModel { public FullChartViewModel() { Albums = new Dictionary>(); Songs = new Dictionary>(); MusicVideos = new Dictionary>(); Playlists = new Dictionary>(); } public Dictionary> MusicVideos { get; set; } public Dictionary> Songs { get; set; } public Dictionary> Albums { get; set; } public Dictionary> Playlists { get; set; } } }