diff --git a/SpotifyAPI.Web/Clients/Interfaces/IPlaylistsClient.cs b/SpotifyAPI.Web/Clients/Interfaces/IPlaylistsClient.cs new file mode 100644 index 00000000..e59e3cb8 --- /dev/null +++ b/SpotifyAPI.Web/Clients/Interfaces/IPlaylistsClient.cs @@ -0,0 +1,9 @@ +using System.Threading.Tasks; + +namespace SpotifyAPI.Web +{ + public interface IPlaylistsClient + { + Task RemoveItems(string playlistId, PlaylistRemoveItemsRequest request); + } +} diff --git a/SpotifyAPI.Web/Clients/Interfaces/IShowsClient.cs b/SpotifyAPI.Web/Clients/Interfaces/IShowsClient.cs new file mode 100644 index 00000000..6eb4f944 --- /dev/null +++ b/SpotifyAPI.Web/Clients/Interfaces/IShowsClient.cs @@ -0,0 +1,15 @@ +using System.Threading.Tasks; + +namespace SpotifyAPI.Web +{ + public interface IShowsClient + { + Task Get(string showId); + Task Get(string showId, ShowRequest request); + + Task GetSeveral(ShowsRequest request); + + Task> GetEpisodes(string showId); + Task> GetEpisodes(string showId, ShowEpisodesRequest request); + } +} diff --git a/SpotifyAPI.Web/Clients/Interfaces/ISpotifyClient.cs b/SpotifyAPI.Web/Clients/Interfaces/ISpotifyClient.cs index 9f368c58..74ef0f08 100644 --- a/SpotifyAPI.Web/Clients/Interfaces/ISpotifyClient.cs +++ b/SpotifyAPI.Web/Clients/Interfaces/ISpotifyClient.cs @@ -5,5 +5,9 @@ namespace SpotifyAPI.Web IUserProfileClient UserProfile { get; } IBrowseClient Browse { get; } + + IShowsClient Shows { get; } + + IPlaylistsClient Playlists { get; } } } diff --git a/SpotifyAPI.Web/Clients/PlaylistsClient.cs b/SpotifyAPI.Web/Clients/PlaylistsClient.cs new file mode 100644 index 00000000..8d32a643 --- /dev/null +++ b/SpotifyAPI.Web/Clients/PlaylistsClient.cs @@ -0,0 +1,19 @@ +using System.Threading.Tasks; +using SpotifyAPI.Web.Http; +using URLs = SpotifyAPI.Web.SpotifyUrls; + +namespace SpotifyAPI.Web +{ + public class PlaylistsClient : APIClient, IPlaylistsClient + { + public PlaylistsClient(IAPIConnector connector) : base(connector) { } + + public Task RemoveItems(string playlistId, PlaylistRemoveItemsRequest request) + { + Ensure.ArgumentNotNullOrEmptyString(playlistId, nameof(playlistId)); + Ensure.ArgumentNotNull(request, nameof(request)); + + return API.Delete(URLs.PlaylistTracks(playlistId), null, request.BuildBodyParams()); + } + } +} diff --git a/SpotifyAPI.Web/Clients/ShowsClient.cs b/SpotifyAPI.Web/Clients/ShowsClient.cs new file mode 100644 index 00000000..c90fcf36 --- /dev/null +++ b/SpotifyAPI.Web/Clients/ShowsClient.cs @@ -0,0 +1,48 @@ +using System.Threading.Tasks; +using SpotifyAPI.Web.Http; +using URLs = SpotifyAPI.Web.SpotifyUrls; + +namespace SpotifyAPI.Web +{ + public class ShowsClient : APIClient, IShowsClient + { + public ShowsClient(IAPIConnector connector) : base(connector) { } + + public Task Get(string showId) + { + Ensure.ArgumentNotNullOrEmptyString(showId, nameof(showId)); + + return API.Get(URLs.Show(showId)); + } + + public Task Get(string showId, ShowRequest request) + { + Ensure.ArgumentNotNullOrEmptyString(showId, nameof(showId)); + Ensure.ArgumentNotNull(request, nameof(request)); + + return API.Get(URLs.Show(showId), request.BuildQueryParams()); + } + + public Task GetSeveral(ShowsRequest request) + { + Ensure.ArgumentNotNull(request, nameof(request)); + + return API.Get(URLs.Shows(), request.BuildQueryParams()); + } + + public Task> GetEpisodes(string showId) + { + Ensure.ArgumentNotNullOrEmptyString(showId, nameof(showId)); + + return API.Get>(URLs.ShowEpisodes(showId)); + } + + public Task> GetEpisodes(string showId, ShowEpisodesRequest request) + { + Ensure.ArgumentNotNullOrEmptyString(showId, nameof(showId)); + Ensure.ArgumentNotNull(request, nameof(request)); + + return API.Get>(URLs.ShowEpisodes(showId), request.BuildQueryParams()); + } + } +} diff --git a/SpotifyAPI.Web/Clients/SpotifyClient.cs b/SpotifyAPI.Web/Clients/SpotifyClient.cs index 46bb45fc..18bfcfae 100644 --- a/SpotifyAPI.Web/Clients/SpotifyClient.cs +++ b/SpotifyAPI.Web/Clients/SpotifyClient.cs @@ -17,10 +17,16 @@ namespace SpotifyAPI.Web _apiConnector = config.CreateAPIConnector(); UserProfile = new UserProfileClient(_apiConnector); Browse = new BrowseClient(_apiConnector); + Shows = new ShowsClient(_apiConnector); + Playlists = new PlaylistsClient(_apiConnector); } public IUserProfileClient UserProfile { get; } public IBrowseClient Browse { get; } + + public IShowsClient Shows { get; } + + public IPlaylistsClient Playlists { get; } } } diff --git a/SpotifyAPI.Web/Clients/SpotifyClientConfig.cs b/SpotifyAPI.Web/Clients/SpotifyClientConfig.cs index 8a922605..2ad56509 100644 --- a/SpotifyAPI.Web/Clients/SpotifyClientConfig.cs +++ b/SpotifyAPI.Web/Clients/SpotifyClientConfig.cs @@ -38,11 +38,11 @@ namespace SpotifyAPI.Web internal IAPIConnector CreateAPIConnector() { - Ensure.PropertyNotNull(BaseAddress, nameof(BaseAddress)); - Ensure.PropertyNotNull(Authenticator, nameof(Authenticator), + Ensure.ArgumentNotNull(BaseAddress, nameof(BaseAddress)); + Ensure.ArgumentNotNull(Authenticator, nameof(Authenticator), ". Use WithToken or WithAuthenticator to specify a authentication"); - Ensure.PropertyNotNull(JSONSerializer, nameof(JSONSerializer)); - Ensure.PropertyNotNull(HTTPClient, nameof(HTTPClient)); + Ensure.ArgumentNotNull(JSONSerializer, nameof(JSONSerializer)); + Ensure.ArgumentNotNull(HTTPClient, nameof(HTTPClient)); return new APIConnector(BaseAddress, Authenticator, JSONSerializer, HTTPClient, RetryHandler); } diff --git a/SpotifyAPI.Web/Http/APIConnector.cs b/SpotifyAPI.Web/Http/APIConnector.cs index 9d56a8b9..b6e9f121 100644 --- a/SpotifyAPI.Web/Http/APIConnector.cs +++ b/SpotifyAPI.Web/Http/APIConnector.cs @@ -135,11 +135,14 @@ namespace SpotifyAPI.Web.Http _jsonSerializer.SerializeRequest(request); await _authenticator.Apply(request).ConfigureAwait(false); IResponse response = await _httpClient.DoRequest(request).ConfigureAwait(false); - response = await _retryHandler?.HandleRetry(request, response, async (newRequest) => + if (_retryHandler != null) { - await _authenticator.Apply(newRequest).ConfigureAwait(false); - return await _httpClient.DoRequest(request).ConfigureAwait(false); - }); + response = await _retryHandler?.HandleRetry(request, response, async (newRequest) => + { + await _authenticator.Apply(newRequest).ConfigureAwait(false); + return await _httpClient.DoRequest(request).ConfigureAwait(false); + }); + } ProcessErrors(response); IAPIResponse apiResponse = _jsonSerializer.DeserializeResponse(response); diff --git a/SpotifyAPI.Web/Models/Request/PlaylistRemoveItemsRequest.cs b/SpotifyAPI.Web/Models/Request/PlaylistRemoveItemsRequest.cs new file mode 100644 index 00000000..effeb2a3 --- /dev/null +++ b/SpotifyAPI.Web/Models/Request/PlaylistRemoveItemsRequest.cs @@ -0,0 +1,28 @@ +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace SpotifyAPI.Web +{ + public class PlaylistRemoveItemsRequest : RequestParams + { + [BodyParam("tracks")] + public List Tracks { get; set; } + [BodyParam("snapshot_id")] + public string SnapshotId { get; set; } + + protected override void CustomEnsure() + { + Ensure.ArgumentNotNullOrEmptyList(Tracks, nameof(Tracks)); + } + + public class Item + { + public Item(string uri) + { + Uri = uri; + } + [JsonProperty("uri")] + public string Uri { get; set; } + } + } +} diff --git a/SpotifyAPI.Web/Models/Request/RecommendationsRequest.cs b/SpotifyAPI.Web/Models/Request/RecommendationsRequest.cs index 774ed041..8e0095b4 100644 --- a/SpotifyAPI.Web/Models/Request/RecommendationsRequest.cs +++ b/SpotifyAPI.Web/Models/Request/RecommendationsRequest.cs @@ -31,7 +31,7 @@ namespace SpotifyAPI.Web public Dictionary Max { get; set; } public Dictionary Target { get; set; } - protected override void Ensure() + protected override void CustomEnsure() { if (string.IsNullOrEmpty(SeedTracks) && string.IsNullOrEmpty(SeedGenres) && string.IsNullOrEmpty(SeedArtists)) { diff --git a/SpotifyAPI.Web/Models/Request/RequestParams.cs b/SpotifyAPI.Web/Models/Request/RequestParams.cs index c6d65421..48a50ac0 100644 --- a/SpotifyAPI.Web/Models/Request/RequestParams.cs +++ b/SpotifyAPI.Web/Models/Request/RequestParams.cs @@ -2,14 +2,38 @@ using System.Reflection; using System; using System.Linq; using System.Collections.Generic; +using System.Collections; +using Newtonsoft.Json.Linq; + namespace SpotifyAPI.Web { public abstract class RequestParams { + public JObject BuildBodyParams() + { + // Make sure everything is okay before building query params + CustomEnsure(); + + var bodyProps = GetType().GetProperties(BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Public) + .Where(prop => prop.GetCustomAttributes(typeof(BodyParamAttribute), true).Length > 0); + + var obj = new JObject(); + foreach (var prop in bodyProps) + { + var attribute = prop.GetCustomAttribute(typeof(BodyParamAttribute)) as BodyParamAttribute; + object value = prop.GetValue(this); + if (value != null) + { + obj[attribute.Key ?? prop.Name] = JToken.FromObject(value); + } + } + return obj; + } + public Dictionary BuildQueryParams() { // Make sure everything is okay before building query params - Ensure(); + CustomEnsure(); var queryProps = GetType().GetProperties(BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Public) .Where(prop => prop.GetCustomAttributes(typeof(QueryParamAttribute), true).Length > 0); @@ -21,7 +45,16 @@ namespace SpotifyAPI.Web object value = prop.GetValue(this); if (value != null) { - queryParams.Add(attribute.Key ?? prop.Name, value.ToString()); + if (value is List) + { + List list = value as List; + var str = string.Join(",", list); + queryParams.Add(attribute.Key ?? prop.Name, str); + } + else + { + queryParams.Add(attribute.Key ?? prop.Name, value.ToString()); + } } } @@ -30,7 +63,7 @@ namespace SpotifyAPI.Web return queryParams; } - protected virtual void Ensure() { } + protected virtual void CustomEnsure() { } protected virtual void AddCustomQueryParams(Dictionary queryParams) { } } @@ -47,6 +80,12 @@ namespace SpotifyAPI.Web public class BodyParamAttribute : Attribute { + public string Key { get; } + public BodyParamAttribute() { } + public BodyParamAttribute(string key) + { + Key = key; + } } } diff --git a/SpotifyAPI.Web/Models/Request/ShowEpisodesRequest.cs b/SpotifyAPI.Web/Models/Request/ShowEpisodesRequest.cs new file mode 100644 index 00000000..5f1748db --- /dev/null +++ b/SpotifyAPI.Web/Models/Request/ShowEpisodesRequest.cs @@ -0,0 +1,14 @@ +namespace SpotifyAPI.Web +{ + public class ShowEpisodesRequest : RequestParams + { + [QueryParam("limit")] + public int? Limit { get; set; } + + [QueryParam("offset")] + public int? Offset { get; set; } + + [QueryParam("market")] + public string Market { get; set; } + } +} diff --git a/SpotifyAPI.Web/Models/Request/ShowRequest.cs b/SpotifyAPI.Web/Models/Request/ShowRequest.cs new file mode 100644 index 00000000..6d632dba --- /dev/null +++ b/SpotifyAPI.Web/Models/Request/ShowRequest.cs @@ -0,0 +1,8 @@ +namespace SpotifyAPI.Web +{ + public class ShowRequest : RequestParams + { + [QueryParam("market")] + public string Market { get; set; } + } +} diff --git a/SpotifyAPI.Web/Models/Request/ShowsRequest.cs b/SpotifyAPI.Web/Models/Request/ShowsRequest.cs new file mode 100644 index 00000000..49fa31e0 --- /dev/null +++ b/SpotifyAPI.Web/Models/Request/ShowsRequest.cs @@ -0,0 +1,18 @@ +using System.Collections.Generic; + +namespace SpotifyAPI.Web +{ + public class ShowsRequest : RequestParams + { + [QueryParam("ids")] + public List Ids { get; set; } + + [QueryParam("market")] + public string Market { get; set; } + + protected override void CustomEnsure() + { + Ensure.ArgumentNotNullOrEmptyList(Ids, nameof(Ids)); + } + } +} diff --git a/SpotifyAPI.Web/Models/Response/FullEpisode.cs b/SpotifyAPI.Web/Models/Response/FullEpisode.cs index f2e032f1..bc29d15e 100644 --- a/SpotifyAPI.Web/Models/Response/FullEpisode.cs +++ b/SpotifyAPI.Web/Models/Response/FullEpisode.cs @@ -1,7 +1,10 @@ using System.Collections.Generic; +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; + namespace SpotifyAPI.Web { - public class FullEpisode : IPlaylistElement + public class FullEpisode : IPlaylistItem { public string AudioPreviewUrl { get; set; } public string Description { get; set; } @@ -19,7 +22,9 @@ namespace SpotifyAPI.Web public string ReleaseDatePrecision { get; set; } public ResumePoint ResumePoint { get; set; } public SimpleShow Show { get; set; } - public ElementType Type { get; set; } + + [JsonConverter(typeof(StringEnumConverter))] + public ItemType Type { get; set; } public string Uri { get; set; } } } diff --git a/SpotifyAPI.Web/Models/Response/FullShow.cs b/SpotifyAPI.Web/Models/Response/FullShow.cs new file mode 100644 index 00000000..b0311391 --- /dev/null +++ b/SpotifyAPI.Web/Models/Response/FullShow.cs @@ -0,0 +1,24 @@ +using System.Collections.Generic; + +namespace SpotifyAPI.Web +{ + public class FullShow + { + public List AvailableMarkets { get; set; } + public List Copyrights { get; set; } + public string Description { get; set; } + public Paging Episodes { get; set; } + public bool Explicit { get; set; } + public Dictionary ExternalUrls { get; set; } + public string Href { get; set; } + public string Id { get; set; } + public List Images { get; set; } + public bool IsExternallyHosted { get; set; } + public List Languages { get; set; } + public string MediaType { get; set; } + public string Name { get; set; } + public string Publisher { get; set; } + public string Type { get; set; } + public string Uri { get; set; } + } +} diff --git a/SpotifyAPI.Web/Models/Response/FullTrack.cs b/SpotifyAPI.Web/Models/Response/FullTrack.cs index 4e722a85..f122304a 100644 --- a/SpotifyAPI.Web/Models/Response/FullTrack.cs +++ b/SpotifyAPI.Web/Models/Response/FullTrack.cs @@ -1,8 +1,10 @@ using System.Collections.Generic; +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; namespace SpotifyAPI.Web { - public class FullTrack : IPlaylistElement + public class FullTrack : IPlaylistItem { public SimpleAlbum Album { get; set; } public List Artists { get; set; } @@ -21,7 +23,9 @@ namespace SpotifyAPI.Web public int Popularity { get; set; } public string PreviewUrl { get; set; } public int TrackNumber { get; set; } - public ElementType Type { get; set; } + + [JsonConverter(typeof(StringEnumConverter))] + public ItemType Type { get; set; } public string Uri { get; set; } public bool IsLocal { get; set; } } diff --git a/SpotifyAPI.Web/Models/Response/Interfaces/IPlaylistElement.cs b/SpotifyAPI.Web/Models/Response/Interfaces/IPlaylistElement.cs index 1534f4f2..254e802c 100644 --- a/SpotifyAPI.Web/Models/Response/Interfaces/IPlaylistElement.cs +++ b/SpotifyAPI.Web/Models/Response/Interfaces/IPlaylistElement.cs @@ -3,15 +3,15 @@ using Newtonsoft.Json.Converters; namespace SpotifyAPI.Web { - public enum ElementType + public enum ItemType { Track, Episode } - public interface IPlaylistElement + public interface IPlaylistItem { [JsonConverter(typeof(StringEnumConverter))] - public ElementType Type { get; set; } + public ItemType Type { get; set; } } } diff --git a/SpotifyAPI.Web/Models/Response/PlaylistTrack.cs b/SpotifyAPI.Web/Models/Response/PlaylistTrack.cs index 2093222f..31072695 100644 --- a/SpotifyAPI.Web/Models/Response/PlaylistTrack.cs +++ b/SpotifyAPI.Web/Models/Response/PlaylistTrack.cs @@ -9,6 +9,6 @@ namespace SpotifyAPI.Web public PublicUser AddedBy { get; set; } public bool IsLocal { get; set; } [JsonConverter(typeof(PlaylistElementConverter))] - public IPlaylistElement Track { get; set; } + public IPlaylistItem Track { get; set; } } } diff --git a/SpotifyAPI.Web/Models/Response/ShowsResponse.cs b/SpotifyAPI.Web/Models/Response/ShowsResponse.cs new file mode 100644 index 00000000..5ed668aa --- /dev/null +++ b/SpotifyAPI.Web/Models/Response/ShowsResponse.cs @@ -0,0 +1,9 @@ +using System.Collections.Generic; + +namespace SpotifyAPI.Web +{ + public class ShowsResponse + { + public List Shows { get; set; } + } +} diff --git a/SpotifyAPI.Web/Models/Response/SimpleEpisode.cs b/SpotifyAPI.Web/Models/Response/SimpleEpisode.cs new file mode 100644 index 00000000..547ca019 --- /dev/null +++ b/SpotifyAPI.Web/Models/Response/SimpleEpisode.cs @@ -0,0 +1,33 @@ +using System; +using System.Collections.Generic; +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; + +namespace SpotifyAPI.Web +{ + public class SimpleEpisode + { + public string AudioPreviewUrl { get; set; } + public string Description { get; set; } + public int DurationMs { get; set; } + public bool Explicit { get; set; } + public Dictionary ExternalUrls { get; set; } + public string Href { get; set; } + public string Id { get; set; } + public List Images { get; set; } + public bool IsExternallyHosted { get; set; } + public bool IsPlayable { get; set; } + + [Obsolete("This field is deprecated and might be removed in the future. Please use the languages field instead")] + public string Language { get; set; } + public List Languages { get; set; } + public string Name { get; set; } + public string ReleaseDate { get; set; } + public string ReleaseDatePrecision { get; set; } + public ResumePoint ResumePoint { get; set; } + + [JsonConverter(typeof(StringEnumConverter))] + public ItemType Type { get; set; } + public string Uri { get; set; } + } +} diff --git a/SpotifyAPI.Web/Models/Response/SimpleShow.cs b/SpotifyAPI.Web/Models/Response/SimpleShow.cs index cde821a5..1654ea2e 100644 --- a/SpotifyAPI.Web/Models/Response/SimpleShow.cs +++ b/SpotifyAPI.Web/Models/Response/SimpleShow.cs @@ -1,10 +1,11 @@ using System.Collections.Generic; + namespace SpotifyAPI.Web { public class SimpleShow { public List AvailableMarkets { get; set; } - public Copyright Copyright { get; set; } + public List Copyrights { get; set; } public string Description { get; set; } public bool Explicit { get; set; } public Dictionary ExternalUrls { get; set; } @@ -16,7 +17,7 @@ namespace SpotifyAPI.Web public string MediaType { get; set; } public string Name { get; set; } public string Publisher { get; set; } - public ElementType Type { get; set; } + public string Type { get; set; } public string Uri { get; set; } } } diff --git a/SpotifyAPI.Web/Models/Response/SimpleTrack.cs b/SpotifyAPI.Web/Models/Response/SimpleTrack.cs index 665ed10d..71462dd0 100644 --- a/SpotifyAPI.Web/Models/Response/SimpleTrack.cs +++ b/SpotifyAPI.Web/Models/Response/SimpleTrack.cs @@ -1,4 +1,6 @@ using System.Collections.Generic; +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; namespace SpotifyAPI.Web { @@ -17,7 +19,9 @@ namespace SpotifyAPI.Web public string Name { get; set; } public string PreviewUrl { get; set; } public int TrackNumber { get; set; } - public ElementType Type { get; set; } + + [JsonConverter(typeof(StringEnumConverter))] + public ItemType Type { get; set; } public string Uri { get; set; } } } diff --git a/SpotifyAPI.Web/Models/Response/SnapshotResponse.cs b/SpotifyAPI.Web/Models/Response/SnapshotResponse.cs new file mode 100644 index 00000000..97ebe950 --- /dev/null +++ b/SpotifyAPI.Web/Models/Response/SnapshotResponse.cs @@ -0,0 +1,7 @@ +namespace SpotifyAPI.Web +{ + public class SnapshotResponse + { + public string SnapshotId { get; set; } + } +} diff --git a/SpotifyAPI.Web/SpotifyUrls.cs b/SpotifyAPI.Web/SpotifyUrls.cs index 57b11819..44ad3bd7 100644 --- a/SpotifyAPI.Web/SpotifyUrls.cs +++ b/SpotifyAPI.Web/SpotifyUrls.cs @@ -25,6 +25,14 @@ namespace SpotifyAPI.Web public static Uri FeaturedPlaylists() => EUri($"browse/featured-playlists"); + public static Uri Show(string showId) => EUri($"shows/{showId}"); + + public static Uri Shows() => EUri($"shows"); + + public static Uri ShowEpisodes(string showId) => EUri($"shows/{showId}/episodes"); + + public static Uri PlaylistTracks(string playlistId) => EUri($"playlists/{playlistId}/tracks"); + private static Uri EUri(FormattableString path) => new Uri(path.ToString(_provider), UriKind.Relative); } } diff --git a/SpotifyAPI.Web/Util/Ensure.cs b/SpotifyAPI.Web/Util/Ensure.cs index 645279c2..d1711f30 100644 --- a/SpotifyAPI.Web/Util/Ensure.cs +++ b/SpotifyAPI.Web/Util/Ensure.cs @@ -1,4 +1,6 @@ using System; +using System.Collections.Generic; +using System.Linq; namespace SpotifyAPI.Web { @@ -12,14 +14,15 @@ namespace SpotifyAPI.Web /// /// The argument value to check /// The name of the argument - public static void ArgumentNotNull(object value, string name) + /// Additional Exception Text + public static void ArgumentNotNull(object value, string name, string additional = null) { if (value != null) { return; } - throw new ArgumentNullException(name); + throw new ArgumentNullException($"{name}{additional}"); } /// @@ -37,14 +40,14 @@ namespace SpotifyAPI.Web throw new ArgumentException("String is empty or null", name); } - public static void PropertyNotNull(object value, string name, string additional = null) + public static void ArgumentNotNullOrEmptyList(List value, string name) { - if (value != null) + if (value != null && value.Any()) { return; } - throw new InvalidOperationException($"The property {name} is null{additional}"); + throw new ArgumentException("List is empty or null", name); } } }