diff --git a/README.md b/README.md index b5c37f0..e1c2e5c 100644 --- a/README.md +++ b/README.md @@ -167,4 +167,4 @@ Huge thanks to **@aevansme**, **@brandongregoryscott** and **@akshays2112** for Contributions welcomed. Read [CONTRIB.md](./CONTRIB.md) -[SpotifyApi.NetCore.Samples]:https://github.com/Ringobot/SpotifyApi.NetCore.Samples +[SpotifyApi.NetCore.Samples]:https://github.com/Ringobot/SpotifyApi.NetCore.Samples \ No newline at end of file diff --git a/src/SpotifyApi.NetCore.Tests/PlaylistsTests.cs b/src/SpotifyApi.NetCore.Tests/PlaylistsTests.cs index 46eacc1..c4c7b64 100644 --- a/src/SpotifyApi.NetCore.Tests/PlaylistsTests.cs +++ b/src/SpotifyApi.NetCore.Tests/PlaylistsTests.cs @@ -252,5 +252,44 @@ public async Task GetPlaylistCoverImage_PlaylistId_AtLeastOnePlalistCoverImageRe var playlistCoverImages = await api.GetPlaylistCoverImage("3cEYpjA9oz9GiPac4AsH4n", accessToken); Assert.IsTrue(playlistCoverImages.Length > 0, "No playlist images were found."); } + + [TestMethod] + [TestCategory("Integration")] + public async Task RemoveItems_PlaylistId_SpotifyUris_SnapshotIdIsNotNull() + { + // act + Assert.IsNotNull(await api.RemoveItems("46sqDMykUCnssu2F3zWXEF", new string[] { "spotify:track:5ArQzSBevAdXTxRY6Ulhbq" }, + accessToken: await TestsHelper.GetUserAccessToken())); + } + + [TestMethod] + [TestCategory("Integration")] + public async Task RemoveItems_PlaylistId_SpotifyUris_SnapshotId_SnapshotIdIsNotNull() + { + // act + Assert.IsNotNull(await api.RemoveItems("46sqDMykUCnssu2F3zWXEF", new string[] { "spotify:track:5ArQzSBevAdXTxRY6Ulhbq" }, + "MTQsMWNlNjE5NWJkYWFjOTNmNDM1M2NjZDM4NGNjMWViMTZlNzk0ZWEyYw==", accessToken: await TestsHelper.GetUserAccessToken())); + } + + [TestMethod] + [TestCategory("Integration")] + public async Task RemoveItems_PlaylistId_SpotifyUriLocations_SnapshotIdIsNotNull() + { + // act + Assert.IsNotNull(await api.RemoveItems("46sqDMykUCnssu2F3zWXEF", + new (string uri, int[] positions)[] { ("spotify:track:1Eolhana7nKHYpcYpdVcT5", new int[] { 0 }) }, + accessToken: await TestsHelper.GetUserAccessToken())); + } + + [TestMethod] + [TestCategory("Integration")] + public async Task RemoveItems_PlaylistId_SpotifyUriLocations_SnapshotId_SnapshotIdIsNotNull() + { + // act + Assert.IsNotNull(await api.RemoveItems("46sqDMykUCnssu2F3zWXEF", + new (string uri, int[] positions)[] { ("spotify:track:7iL6o9tox1zgHpKUfh9vuC", new int[] { 0 }) }, + "MTQsMWNlNjE5NWJkYWFjOTNmNDM1M2NjZDM4NGNjMWViMTZlNzk0ZWEyYw==", accessToken: await TestsHelper.GetUserAccessToken())); + } + } } diff --git a/src/SpotifyApi.NetCore.Tests/TracksApiTests.cs b/src/SpotifyApi.NetCore.Tests/TracksApiTests.cs index 2cf24ff..5242153 100644 --- a/src/SpotifyApi.NetCore.Tests/TracksApiTests.cs +++ b/src/SpotifyApi.NetCore.Tests/TracksApiTests.cs @@ -52,8 +52,8 @@ public async Task GetTrack_TrackIdNoMarket_MarketsArrayExists() public async Task GetTrack_TrackIdMarket_AvailableMarketsIsNull() { // arrange - const string trackId = "5lA3pwMkBdd24StM90QrNR"; - const string market = SpotifyCountryCodes.New_Zealand; + const string trackId = "11dFghVXANMlKmJXsNCbd8"; + const string market = SpotifyCountryCodes.Spain; var http = new HttpClient(); var accounts = new AccountsService(http, TestsHelper.GetLocalConfig()); @@ -64,7 +64,7 @@ public async Task GetTrack_TrackIdMarket_AvailableMarketsIsNull() var response = await api.GetTrack(trackId, market); // assert - Assert.IsNull(response.AvailableMarkets); + Assert.IsNull(response?.AvailableMarkets?.Length == 0 ? null : "Array not empty."); } [TestCategory("Integration")] diff --git a/src/SpotifyApi.NetCore/Models/PlaylistRemoveItemsPayloadData.cs b/src/SpotifyApi.NetCore/Models/PlaylistRemoveItemsPayloadData.cs new file mode 100644 index 0000000..e6730d5 --- /dev/null +++ b/src/SpotifyApi.NetCore/Models/PlaylistRemoveItemsPayloadData.cs @@ -0,0 +1,56 @@ +using Newtonsoft.Json; +using System.Collections.Generic; + +namespace SpotifyApi.NetCore.Models +{ + public partial class PlaylistRemoveItemsPayloadDataUriItems + { + + public PlaylistRemoveItemsPayloadDataUriItems(string[] uris, string snapshotId = null) + { + Uris = new PlaylistRemoveItemsPayloadDataUriItem[uris.Length]; + for (int i = 0; i < Uris.Length; i++) + { + Uris[i] = new PlaylistRemoveItemsPayloadDataUriItem(uris[i]); + } + SnapshotId = snapshotId; + } + + public PlaylistRemoveItemsPayloadDataUriItems((string uri, int[] positions)[] uriPositions, string snapshotId = null) + { + Uris = new PlaylistRemoveItemsPayloadDataUriItem[uriPositions.Length]; + for (int i = 0; i < Uris.Length; i++) + { + Uris[i] = new PlaylistRemoveItemsPayloadDataUriItem(uriPositions[i]); + } + SnapshotId = snapshotId; + } + + [JsonProperty("tracks", NullValueHandling = NullValueHandling.Ignore)] + public PlaylistRemoveItemsPayloadDataUriItem[] Uris { get; set; } + + [JsonProperty("snapshot_id", NullValueHandling = NullValueHandling.Ignore)] + public string SnapshotId { get; set; } + + } + + public partial class PlaylistRemoveItemsPayloadDataUriItem + { + + public PlaylistRemoveItemsPayloadDataUriItem(string uri) => this.Uri = uri; + + public PlaylistRemoveItemsPayloadDataUriItem((string uri, int[] positions) uriPositions) + { + Uri = uriPositions.uri; + Positions = uriPositions.positions; + } + + [JsonProperty("uri")] + public string Uri { get; set; } + + [JsonProperty("positions", NullValueHandling = NullValueHandling.Ignore)] + public int[] Positions { get; set; } + + } + +} diff --git a/src/SpotifyApi.NetCore/PlaylistsApi.cs b/src/SpotifyApi.NetCore/PlaylistsApi.cs index 65d0e7d..86ff84a 100644 --- a/src/SpotifyApi.NetCore/PlaylistsApi.cs +++ b/src/SpotifyApi.NetCore/PlaylistsApi.cs @@ -5,6 +5,7 @@ using System.Threading.Tasks; using Newtonsoft.Json; using System.Text; +using SpotifyApi.NetCore.Models; namespace SpotifyApi.NetCore { @@ -464,11 +465,21 @@ public async Task GetPlaylistCoverImage( /// /// https://developer.spotify.com/documentation/web-api/reference/playlists/remove-tracks-playlist/ /// - public Task RemoveItems( + public async Task RemoveItems( string playlistId, string[] spotifyUris, string snapshotId = null, - string accessToken = null) => throw new NotImplementedException(); + string accessToken = null) + { + if (string.IsNullOrWhiteSpace(playlistId)) throw new + ArgumentException("A valid Spotify playlist id must be specified."); + + if (spotifyUris?.Length < 1 || spotifyUris?.Length > 100) throw new + ArgumentException("A minimum of 1 and a maximum of 100 Spotify uri must be specified."); + + var builder = new UriBuilder($"{BaseUrl}/playlists/{playlistId}/tracks"); + return (await Delete(builder.Uri, new PlaylistRemoveItemsPayloadDataUriItems(spotifyUris, snapshotId), accessToken)).Data; + } /// /// Remove one or more items from a user’s playlist. @@ -485,11 +496,21 @@ public Task RemoveItems( /// /// https://developer.spotify.com/documentation/web-api/reference/playlists/remove-tracks-playlist/ /// - public Task RemoveItems( + public async Task RemoveItems( string playlistId, (string uri, int[] positions)[] spotifyUriPositions, string snapshotId = null, - string accessToken = null) => throw new NotImplementedException(); + string accessToken = null) + { + if (string.IsNullOrWhiteSpace(playlistId)) throw new + ArgumentException("A valid Spotify playlist id must be specified."); + + if (spotifyUriPositions?.Length < 1 || spotifyUriPositions?.Length > 100) throw new + ArgumentException("A minimum of 1 and a maximum of 100 Spotify uri and positions must be specified."); + + var builder = new UriBuilder($"{BaseUrl}/playlists/{playlistId}/tracks"); + return (await Delete(builder.Uri, new PlaylistRemoveItemsPayloadDataUriItems(spotifyUriPositions, snapshotId), accessToken)).Data; + } #endregion diff --git a/src/SpotifyApi.NetCore/SpotifyApi.NetCore.csproj b/src/SpotifyApi.NetCore/SpotifyApi.NetCore.csproj index fb7e6e3..436dd32 100644 --- a/src/SpotifyApi.NetCore/SpotifyApi.NetCore.csproj +++ b/src/SpotifyApi.NetCore/SpotifyApi.NetCore.csproj @@ -26,6 +26,6 @@ - + diff --git a/src/SpotifyApi.NetCore/SpotifyWebApi.cs b/src/SpotifyApi.NetCore/SpotifyWebApi.cs index a0b002b..4435361 100644 --- a/src/SpotifyApi.NetCore/SpotifyWebApi.cs +++ b/src/SpotifyApi.NetCore/SpotifyWebApi.cs @@ -180,6 +180,54 @@ protected internal virtual async Task Post(Uri uri, object data protected internal virtual async Task> Post(Uri uri, object data, string accessToken = null) => await PostOrPut("POST", uri, data, accessToken); + /// + /// Helper to DELETE an object with content to put in request body. + /// + protected internal virtual async Task> Delete(Uri uri, object data, string accessToken = null) + { + Logger.Debug($"DELETE {uri}. Token = {accessToken?.ToString()?.Substring(0, 4)}...", nameof(SpotifyWebApi)); + + _http.DefaultRequestHeaders.Authorization = + new AuthenticationHeaderValue("Bearer", accessToken ?? (await GetAccessToken())); + + StringContent content = null; + + if (data == null) + { + content = null; + } + else + { + content = new StringContent(JsonConvert.SerializeObject(data)); + content.Headers.ContentType = new MediaTypeHeaderValue("application/json"); + } + + HttpResponseMessage response = null; + + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Delete, uri); + request.Content = content; + + response = await _http.SendAsync(request); + + Logger.Information($"DELETE {uri} {response.StatusCode}", nameof(RestHttpClient)); + + await RestHttpClient.CheckForErrors(response); + + var spotifyResponse = new SpotifyResponse + { + StatusCode = response.StatusCode, + ReasonPhrase = response.ReasonPhrase + }; + + if (response.Content != null) + { + string json = await response.Content.ReadAsStringAsync(); + if (!string.IsNullOrEmpty(json)) spotifyResponse.Data = JsonConvert.DeserializeObject(json); + } + + return spotifyResponse; + } + /// /// Helper to DELETE an object ///