Coder Social home page Coder Social logo

daltron / spartan Goto Github PK

View Code? Open in Web Editor NEW
110.0 9.0 16.0 1.39 MB

An Elegant Spotify Web API Library Written in Swift for iOS and macOS

License: MIT License

Ruby 0.63% Swift 99.37%
spotify spotify-api spotify-library spotify-alternative ios macos swift

spartan's Introduction

Spartan

CI Status Version Language: Swift License Platform

Written in Swift 4.2

Spartan is a lightweight, elegant, and easy to use Spotify Web API wrapper library for iOS and macOS written in Swift 3. Under the hood, Spartan makes request to the Spotify Web API. Those requests are then turned into clean, friendly, and easy to use objects.

What this library allows you to do:

  • โœ… Make any request that the Spotify Web API allows.

What this library doesn't do:

  • โŒ Authorization flows that help provide you with an authorization token. The Spotify iOS SDK can help assist you with that.

Requirements

  • iOS 9.0+ / macOS 10.11+
  • xCode 9.0+

Installation

CocoaPods

To integrate Spartan into your xCode project using CocoaPods, specify it in your Podfile:

source 'https://github.com/CocoaPods/Specs.git'
platform :ios, '9.0'   # If you targeting iOS
platform :osx, '10.11' # If you targeting macOS
use_frameworks!

pod 'Spartan'

Then, run the following command:

$ pod install

This will download any library dependencies you do not already have in your project.

Usage

Spartan Properties

Authorization Token

public static var authorizationToken: String?

This is the token that is included with each request that requires OAuth authentication. If the request you make requires OAuth and this in nil, invalid, or expired, the request will fail. This can be left nil if you do not plan on making any requests that require OAuth authentication.

Each time your token is refreshed, simply update it:

Spartan.authorizationToken = newToken

Logging

public static var loggingEnabled: Bool = true

When enabled, before each request starts and after each request finishes, helpful statements will be printed to the console.

A successful request will look something like this:

๐Ÿ”ต [SpartanRequestLogger] GET https://api.spotify.com/v1/me
โšช๏ธ [SpartanRequestLogger] GET https://api.spotify.com/v1/me (200 OK) 0.140969038009644 seconds

While a failed/invalid request will look something like this:

๐Ÿ”ต [SpartanRequestLogger] GET https://api.spotify.com/v1/me
๐Ÿ”ด [SpartanRequestLogger] GET https://api.spotify.com/v1/me (401 Unauthorized) 0.81086003780365 seconds

This can be enabled/disabled at any time anywhere in your code simply by:

Spartan.loggingEnabled = true/false

Core

For more information on each request, please click the link associated with it. Spotify provides excellent documentation for each api request that they support. Spartan supports almost all query parameter fields that Spotify allows.

PagingObject

Quite a few requests return a PagingObject. This is an offset based paging object that is a container for a set of objects. It contains a key called items whose value is an array of the requested objects. It can be used to get the previous set of items or the next set of items for a future call.

For example:

_ = Spartan.search(query: "Five for Fighting", type: .track, success: { (pagingObject: PagingObject<Track>) in
	self.pagingObject = pagingObject
}, failure: { (error) in
	print(error)
})

Then later, if you need to load more data (scrolled to bottom of UITableView/UICollectionView, etc...) more data can be loaded/reloaded with these two methods:

Get Next

if pagingObject.canMakeNextRequest {

	pagingObject.getNext(success: { (pagingObject) in
   		// Update the paging object
   		self.pagingObject = pagingObject            
	}, failure: { (error) in
		print(error)
	})
}

Get Previous

if pagingObject.canMakePreviousRequest {

	pagingObject.getPrevious(success: { (pagingObject) in
   		// Update the paging object
   		self.pagingObject = pagingObject            
	}, failure: { (error) in
		print(error)
	})
}

Requests

_ = Spartan.getAlbum(id: albumId, market: .us, success: { (album) in
	// Do something with the album    
}, failure: { (error) in
	print(error)      
})
_ = Spartan.getAlbums(ids: albumIds, market: .us, success: { (albums) in
	// Do something with the albums
}, failure: { (error) in
	print(error)
})
_ = Spartan.getTracks(albumId: albumId, limit: 20, offset: 0, market: .us, success: { (pagingObject) in
	// Get the tracks via pagingObject.items
}, failure: { (error) in
	print(error)
})
_ = Spartan.getArtist(id: artistId, success: { (artist) in
	// Do something with the artist
}, failure: { (error) in
	print(error)
})
_ = Spartan.getArtists(ids: artistIds, success: { (artists) in
	// Do something with the artists
}, failure: { (error) in
	print(error)
})
_ = Spartan.getTrack(id: track, market: .us, success: { (track) in
	// Do something with the track
}, failure: { (error) in
	print(error)
})
_ = Spartan.getTracks(ids: trackIds, market: .us, success: { (tracks) in
	// Do something with the tracks
}, failure: { (error) in
	print(error)
})
_ = Spartan.getArtistAlbums(artistId: artistId, limit: 20, offset: 0, albumTypes: [.album, .single, .appearsOn, .compilation], market: .us, success: { (pagingObject) in
	// Get the albums via pagingObject.items
}, failure: { (error) in
	print(error)
})
 _ = Spartan.getArtistsTopTracks(artistId: artistId, country: .us, success: { (tracks) in
	// Do something with the tracks
}, failure: { (error) in
	print(error)
})
_ = Spartan.getArtistsRelatedArtists(artistId: artistId, success: { (artists) in
	// Do something with the artists
}, failure: { (error) in
	print(error)
})
_ = Spartan.search(query: query, type: .album, success: { (pagingObject: PagingObject<SimplifiedAlbum>) in
	// Get the albums via pagingObject.items     
}, failure: { (error) in
	print(error)
})
_ = Spartan.search(query: query, type: .artist, success: { (pagingObject: PagingObject<SimplifiedArtist>) in
	// Get the artists via pagingObject.items     
}, failure: { (error) in
	print(error)
})
_ = Spartan.search(query: query, type: .playlist, success: { (pagingObject: PagingObject<SimplifiedPlaylist>) in
	// Get the playlists via pagingObject.items     
}, failure: { (error) in
	print(error)
})
_ = Spartan.search(query: query, type: .track, success: { (pagingObject: PagingObject<SimplifiedTrack>) in
	// Get the tracks via pagingObject.items     
}, failure: { (error) in
	print(error)
})
_ = Spartan.getUser(id: userId, success: { (user) in
	// Do something with the user
}, failure: { (error) in
	print(error)
})
_ = Spartan.getAudioAnaylsis(trackId: trackId, success: { (audiAnalysis) in
	// Do something with the audio analysis
}, failure: { (error) in
	print(error)
})
_ = Spartan.getAudioFeatures(trackId: trackId, success: { (audioFeaturesObject) in
	// Do something with the audio features object  
}, failure: { (error) in
	print(error)
})
_ = Spartan.getAudioFeatures(trackId: trackId, success: { (audioFeaturesObject) in
	// Do something with the audio features object  
}, failure: { (error) in
	print(error)
})
_ = Spartan.getAudioFeatures(trackIds: trackIds, success: { (audioFeaturesObject) in
	// Do something with the audio features objects
}, failure: { (error) in
	print(error)
})
_ = Spartan.getFeaturedPlaylists(locale: locale, country: .us, timestamp: timestamp, limit: 20, offset: 0, success: { (featuredPlaylistsObject) in
	// Do something with the featured playlists object        
}, failure: { (error) in
	print(error)        
})
_ = Spartan.getNewReleases(country: .us, limit: 20, offset: 0, success: { (pagingObject) in
	// Get the albums via pagingObject.items
}, failure: { (error) in
	print(error)
})
_ = Spartan.getCategories(success: { (pagingObject) in
	// Get the categories via pagingObject.items
}, failure: { (error) in
	print(error)
})
_ = Spartan.getCategorysPlaylists(categoryId: categoryId, locale: locale, country: .us, limit: 20, offset: 0, success: { (pagingObject) in
	// Get the playlists via pagingObject.items
}, failure: { (error) in
	print(error)
})
_ = Spartan.getMe(success: { (user) in
	// Do something with the user object
}, failure: { (error) in
	print(error)
})
_ = Spartan.getMyFollowedArtists(success: { (pagingObject) in
	// Get artists via pagingObject.items
}, failure: { (error) in
	print(error)
})
_ = Spartan.follow(ids: artistIds, type: .artist, success: {
	// Artists are now followed   
}, failure: { (error) in
	print(error)
})
_ = Spartan.follow(ids: userIds, type: .user, success: {
	// Users are now followed   
}, failure: { (error) in
	print(error)
})
_ = Spartan.unfollow(ids: artistIds, type: .artist, success: {
	// Artists are now unfollowed   
}, failure: { (error) in
	print(error)
})
_ = Spartan.unfollow(ids: userIds, type: .user, success: {
	// Users are now unfollowed   
}, failure: { (error) in
	print(error)
})
_ = Spartan.getIsFollowing(ids: artistIds, type: .artist, success: { (followingBools) in
	// Do something with the followingBools
}, failure: { (error) in
	print(error)        
})
_ = Spartan.getIsFollowing(ids: userIds, type: .user, success: { (followingBools) in
	// Do something with the followingBools
}, failure: { (error) in
	print(error)        
})
_ = Spartan.followPlaylist(ownerId: ownerId, playlistId: playlistId, isPublic: true, success: {
   // Playlist is now followed
}, failure: { (error) in
	print(error)           
})
_ = Spartan.unfollowPlaylist(ownerId: ownerId, playlistId: playlistId, success: {
	// Playlist is now unfollowed     
}, failure: { (error) in
	print(error)
})
 _ = Spartan.getSavedTracks(limit: 20, offset: 0, market: .us, success: { (pagingObject) in
	// Get the saved tracks via pagingObject.items     
}, failure: { (error) in
	print(error)   
})
_ = Spartan.saveTracks(trackIds: trackIds, success: {
	// Tracks are now saved    
}, failure: { (error) in
	print(error)    
})
_ = Spartan.removeSavedTracks(trackIds: trackIds, success: {
	// Tracks are now removed
}, failure: { (error) in
    print(error)
})
_ = Spartan.tracksAreSaved(trackIds: trackIds, success: { (savedBools) in
    // Do something with the savedBools    
}, failure: { (error) in
    print(error)    
})
 _ = Spartan.getSavedAlbums(limit: 20, offset: 0, market: .us, success: { (pagingObject) in
    // Get the saved albums via pagingObject.items    
}, failure: { (error) in
 	print(error)      
})        
_ = Spartan.saveAlbums(albumIds: albumIds, success: {
	// Albums are now saved    
}, failure: { (error) in
	print(error)    
})
_ = Spartan.removeSavedAlbums(albumIds: albumIds, success: {
	// Albums are now removed
}, failure: { (error) in
    print(error)
})
_ = Spartan.albumsAreSaved(albumIds: albumIds, success: { (savedBools) in
    // Do something with the savedBools    
}, failure: { (error) in
    print(error)    
})
_ = Spartan.albumsAreSaved(albumIds: albumIds, success: { (savedBools) in
    // Do something with the savedBools    
}, failure: { (error) in
    print(error)    
})
_ = Spartan.albumsAreSaved(albumIds: albumIds, success: { (savedBools) in
    // Do something with the savedBools    
}, failure: { (error) in
    print(error)    
})
_ = Spartan.getMyTopArtists(limit: 20, offset: 0, timeRange: .mediumTerm, success: { (pagingObject) in
	// Get the artists via pagingObject.items
}, failure: { (error) in
	print(error)
})
_ = Spartan.getMyTopTracks(limit: 20, offset: 0, timeRange: .mediumTerm, success: { (pagingObject) in
	// Get the tracks via pagingObject.items
}, failure: { (error) in
	print(error)
})
_ = Spartan.getUsersPlaylists(userId: userId, limit: 20, offset: 0, success: { (pagingObject) in
	// Get the playlists via pagingObject.playlists
}, failure: { (error) in
	print(error)
})
_ = Spartan.getMyPlaylists(limit: 20, offset: 0, success: { (pagingObject) in
	// Get the playlists via pagingObject.items
}, failure: { (error) in
	print(error)
})
_ = Spartan.getUsersPlaylist(userId: userId, playlistId: playlistId, fields: fields, market: .us, success: { (playlist) in
	// Do something with the playlist
}, failure: { (error) in
	print(error)
})
_ = Spartan.getPlaylistTracks(userId: userId, playlistId: playlistId, limit: 20, offset: 0, fields: fields, market: .us, success: { (pagingObject) in
	// Get the playlist tracks via pagingObject.items
}, failure: { (error) in
	print(error)
})
_ = Spartan.createPlaylist(userId: userId, name: name, isPublic: true, isCollaborative: true, success: { (playlist) in
	// Do something with the playlist
}, failure: { (error) in
	print(error)
})
_ = Spartan.changePlaylistDetails(userId: userId, playlistId: playlistId, name: name, isPublic: false, isCollaborative: false, success: {
	// The playlist details are now changed
}, failure: { (error) in
	print(error)
})
_ = Spartan.addTracksToPlaylist(userId: userId, playlistId: playlistId, trackUris: trackUris, success: { (snapshot) in
	// Do something with the snapshot
}, failure: { (error) in
	print(error)
})
_ = Spartan.removeTracksFromPlaylist(userId: userId, playlistId: playlistId, trackUris: trackUris, success: { (snapshot) in
	// Do something with the snapshot
}, failure: { (error) in
	print(error)
})
_ = Spartan.reorderPlaylistsTracks(userId: userId, playlistId: playlistId, rangeStart: rangeStart, rangeLength: rangeLength, insertBefore: insertBefore, snapshotId: snapshotId, success: { (snapshot) in
	// Tracks are now reordered
}, failure: { (error) in
	print(error)
})
_ = Spartan.replacePlaylistsTracks(userId: userId, playlistId: playlistId, trackUris: trackUris, success: {
	// Tracks are now replaced in playlist
}, failure: { (error) in
	print(error)
})
_ = Spartan.getUsersAreFollowingPlaylists(ownerId: ownerId, playlistId: playlistId, userIds: userIds, success: { (followings) in
	// Do something with the followings
}) { (error) in
	print(error)
}

Handling Errors

SpartanError objects have a type and error message to help determine what kind of error occurred.

public class SpartanError: NSObject, Mappable {
    private(set) open var type: SpartanErrorType!
    private(set) open var errorMessage:String!
}

If a request suceeds but is invalid, the erorrMessage will be the error message returned directly from Spotify.

For example, if the Spotify error response is:

{
  "error": {
    "status": 401,
    "message": "Invalid access token"
  }
}

The SpartanError object within a failure callback will be:

_ = Spartan.getMe(success: { (user) in

}, failure: { (error) in
	print(error.errorType)     // .unauthorized
	print(error.errorMessage)  // "Invalid access token"
})

So if your access token is expired, you could do something like this:

_ = Spartan.getMe(success: { (user) in

}, failure: { (error) in
	if error.errorType == .unauthorized {
		// Refresh your access token and try again
	}
})

Library Dependencies

Usage Information and Limits

Since Spartan is built on top of the Spotify Web API, click here for more information

Author

Dalton Hinterscher, [email protected]

License

Spartan is available under the MIT license. See the LICENSE file for more info.

spartan's People

Contributors

daltron avatar gustavo-carvalho avatar mindaugasjucius avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

spartan's Issues

getTrack() returns a Track only filled with SimplifiedTrack data

Spartan.getTrack(id: "11dFghVXANMlKmJXsNCbNl", success: { (track) in self.currentSong = track // FIXME: gets the album instead of the song self.miniPlayer?.configure(song: self.currentSong) }) { (error) in print("Error while fetching the track: \(error.description)") }
The above code is the one I'm currently using to retrieve a Track, but no album data is filled in it, is this behaviour intentional?

captura de pantalla 2018-11-30 a las 17 44 51

This is the Track returned by Spartan

Using the latest version available in Cocoapods, Swift 4.0 and Xcode 10.1

How to use Spartan

I am creating an app that exports playlists to Spotify. Currently, I have an array of Spotify song URIs. I have no knowledge of how the Spotify SDK works and I was wondering how I would use Spartan to a) set up a connection with the user and 2) create a playlist from the app.

Example Not working

Kindly share working demo project. This Demo project is completely blank.

Swift 4 AlamofireObjectMapper Dependency

Hello,
Just writing to say that the Podfile spec should probably be updated for AlamofireObjectMapper 5 so that users can use swift 4

[!] Unable to satisfy the following requirements:

- `AlamofireObjectMapper (from `https://github.com/tristanhimmelman/AlamofireObjectMapper.git`, branch `swift-4`)` required by `Podfile`
- `AlamofireObjectMapper (= 5.0.0)` required by `Podfile.lock`
- `AlamofireObjectMapper (~> 4.0.1)` required by `Spartan (1.0)`

Playlist Image Issue

Hi Daltron,

I found an issue with playlist images. After getting root user's playlists using Spartan.getMyPlaylists(), for playlists that don't have an image, I get a random image from one of my other playlists. Moreover, all playlists after that one return random images from other playlists even though they have their own image on Spotify. Any help on this would be much appreciated!

Thanks :)

getNext doesn't appear to be working on getMyFollowedArtists

I'm trying to use if object.canMakeNextRequest to get all possible artists returned by getMyFollowedArtists however the second request doesn't appear to be returning an object with any items in it. I've tested my code with the getMyTopArtists method and it works properly. I've also manually changed the limit on getMyFollowedArtists to ensure that the API will return more than the default limit, and it does.

Here's a sample of my code just to see what I am doing.

    var artists: PagingObject<Artist>? {
        didSet {
            print("Added more to artists variable.")
        }
    }

    func grabSpotifyArtists(token: String?) {
        Spartan.authorizationToken = token
        Spartan.loggingEnabled = true
        
        _ = Spartan.getMyFollowedArtists(limit: 10, after: nil, success: { (object) in
            self.artists = object
            self.printArtists()
            self.fetchAllItems()
        }, failure: { (error) in
            print(error)
        })
        

    }
    
    func fetchAllItems() {
        if let artists = self.artists {
            if artists.canMakeNextRequest {
                artists.getNext(success: { (object) in
                    self.artists = object
                    self.printArtists()
                    self.fetchAllItems()
                }) { (error) in
                    print(error)
                }
            }
        }
    }
    
    func printArtists() {
        if let artists = self.artists?.items {
            for artist in artists {
                print(artist.name)
            }
        }
    }

Sample output:

๐Ÿ”ต [AlamoRecordLogger] GET https://api.spotify.com/v1/me/following?type=artist&limit=10
โšช๏ธ [AlamoRecordLogger] GET https://api.spotify.com/v1/me/following?type=artist&limit=10 (200 OK) 0.15 seconds
Added more to artists variable.
Optional("Handsome Furs")
Optional("Nevermen")
Optional("White Denim")
Optional("Quilt")
Optional("The Poison Control Center")
Optional("The Courtneys")
Optional("Meat Wave")
Optional("The Spook School")
Optional("Savages")
Optional("Twin Peaks")
๐Ÿ”ต [AlamoRecordLogger] GET https://api.spotify.com/v1/me/following?type=artist&after=1xD85sp0kecIVuMwUHShxs&limit=10
โšช๏ธ [AlamoRecordLogger] GET https://api.spotify.com/v1/me/following?type=artist&after=1xD85sp0kecIVuMwUHShxs&limit=10 (200 OK) 0.08 seconds
Added more to artists variable.

What I'd expect to happen is the what happens if I use the getMyTopArtists method like so... changed code following way:

    func grabSpotifyArtists(token: String?) {
        Spartan.authorizationToken = token
        Spartan.loggingEnabled = true

        _ = Spartan.getMyTopArtists(limit: 10, offset: 0, timeRange: .longTerm, success: { (object) in
            self.artists = object
            self.printArtists()
            self.fetchAllItems()
        }, failure: { (error) in
            print(error)
        })

    }

Output:

๐Ÿ”ต [AlamoRecordLogger] GET https://api.spotify.com/v1/me/top/artists?limit=10&offset=0&time_range=long_term
โšช๏ธ [AlamoRecordLogger] GET https://api.spotify.com/v1/me/top/artists?limit=10&offset=0&time_range=long_term (200 OK) 0.67 seconds
Added more to artists variable.
Optional("Guided By Voices")
Optional("Lana Del Rey")
Optional("Meat Wave")
Optional("YACHT")
Optional("White Denim")
Optional("Spoon")
Optional("Electric Six")
Optional("Television")
Optional("Quilt")
Optional("Savages")
๐Ÿ”ต [AlamoRecordLogger] GET https://api.spotify.com/v1/me/top/artists?limit=10&offset=10&time_range=long_term
โšช๏ธ [AlamoRecordLogger] GET https://api.spotify.com/v1/me/top/artists?limit=10&offset=10&time_range=long_term (200 OK) 0.19 seconds
Added more to artists variable.
Optional("Sleater-Kinney")
Optional("Queens of the Stone Age")
Optional("Local H")
Optional("Nick Lowe")
Optional("EMA")
Optional("Kishi Bashi")
Optional("Swearin\'")
Optional("BOYTOY")
Optional("Saint Motel")
Optional("Sleigh Bells")
๐Ÿ”ต [AlamoRecordLogger] GET https://api.spotify.com/v1/me/top/artists?limit=10&offset=20&time_range=long_term
โšช๏ธ [AlamoRecordLogger] GET https://api.spotify.com/v1/me/top/artists?limit=10&offset=20&time_range=long_term (200 OK) 0.19 seconds
Added more to artists variable.
Optional("HAIM")
Optional("Wampire")
Optional("White Fence")
Optional("The Spook School")
Optional("Vampire Weekend")
Optional("Wolf Alice")
Optional("Woods")
Optional("Algebra Suicide")
Optional("Foxygen")
Optional("Twin Peaks")
๐Ÿ”ต [AlamoRecordLogger] GET https://api.spotify.com/v1/me/top/artists?limit=10&offset=30&time_range=long_term
โšช๏ธ [AlamoRecordLogger] GET https://api.spotify.com/v1/me/top/artists?limit=10&offset=30&time_range=long_term (200 OK) 0.18 seconds
Added more to artists variable.
Optional("Joanna Gruesome")
Optional("Chastity Belt")
Optional("The High Strung")
Optional("Metz")
Optional("Archie Bronson Outfit")
Optional("Alex Winston")
Optional("Faith No More")
Optional("Posse")
Optional("Todd Terje")
Optional("Museum Mouth")
๐Ÿ”ต [AlamoRecordLogger] GET https://api.spotify.com/v1/me/top/artists?limit=10&offset=40&time_range=long_term
โšช๏ธ [AlamoRecordLogger] GET https://api.spotify.com/v1/me/top/artists?limit=10&offset=40&time_range=long_term (200 OK) 0.18 seconds
Added more to artists variable.
Optional("Andrew Bird")
Optional("Phoenix")
Optional("Evans The Death")
Optional("California X")
Optional("Nine Inch Nails")
Optional("Richard Hell")
Optional("Harry Nilsson")
Optional("FIDLAR")
Optional("Kitten")
Optional("Rockpile")

Suddenly Spartan started crashing my app

I'm not sure if something changed in Spotify API, but performing some requests causes bad crashes. For example, requesting a track with func getTrack(id: String returns invalid object with missing values, for example durationMs is nil, but it never should be. A lot of users are reporting that app started crashing.

Zrzut ekranu 2020-04-12 o 10 38 30

Thread 1: Fatal error: Unexpectedly found nil while unwrapping an Optional value

I am executing the following code and I am getting a Fatal error: Unexpectedly found nil while unwrapping an Optional value that references line 57 of SpartanError. Please see the following screenshot for more detailed thread information. This is the only function that does this. It appears that the SpartanError is trying to map a status code and that it is actually nil. Any help on this would be great.

determineErrorType(statusCode: statusCode)

Code

_ = Spartan.getAudioFeatures(trackIds: tracksIDs, success: { (audioFeatures) in
       print(audioFeatures.first?.tempo)
}, failure: { (error) in
	Logger.error(error)
})

Console Output

๐Ÿ”ต [AlamoRecordLogger] GET https://api.spotify.com/v1/audio-features?ids=7sO5G9EABYOXQKNPNiE9NR%2C0NKh1STZG1VgnVwntJF3ze%2C4dVpf9jZjcORqGTLUaeYj9%2C2Xqd0wUttjueBfdcltADOv%2C0Gux2yTMWYOlcBUNjGJu5p%2C2vjmyyAnDUzilFYxOlTdO1%2C1OmcAT5Y8eg5bUPv9qJT4R%2C6foxplXS0YEq8cO374Lty4%2C5u6vkDnOyaf8LsteDAj2ub%2C1XRgIKC5TPwo7nWGyKqgG0%2C7zLGHiDWd9T1Rxw4PQCb13%2C1YZfcVLbbJwfizR5cDOp3q%2C7r6LNJT2LqpLpEyZQJPygt%2C57IRaiAB4hBZu3gnNVZC0v%2C6GNifiuBPrKFpwNBYnooFm%2C3ncgNpxLoBQ65ABk4djDyd%2C2vaMWMPMgsWX4fwJiKmdWm%2C0taOCiup4HNG9LmbduVlJj%2C59J5nzL1KniFHnU120dQzt%2C4EsYkJjHKMejYLp54woB9c
โšช๏ธ [AlamoRecordLogger] GET https://api.spotify.com/v1/audio-features?ids=7sO5G9EABYOXQKNPNiE9NR%2C0NKh1STZG1VgnVwntJF3ze%2C4dVpf9jZjcORqGTLUaeYj9%2C2Xqd0wUttjueBfdcltADOv%2C0Gux2yTMWYOlcBUNjGJu5p%2C2vjmyyAnDUzilFYxOlTdO1%2C1OmcAT5Y8eg5bUPv9qJT4R%2C6foxplXS0YEq8cO374Lty4%2C5u6vkDnOyaf8LsteDAj2ub%2C1XRgIKC5TPwo7nWGyKqgG0%2C7zLGHiDWd9T1Rxw4PQCb13%2C1YZfcVLbbJwfizR5cDOp3q%2C7r6LNJT2LqpLpEyZQJPygt%2C57IRaiAB4hBZu3gnNVZC0v%2C6GNifiuBPrKFpwNBYnooFm%2C3ncgNpxLoBQ65ABk4djDyd%2C2vaMWMPMgsWX4fwJiKmdWm%2C0taOCiup4HNG9LmbduVlJj%2C59J5nzL1KniFHnU120dQzt%2C4EsYkJjHKMejYLp54woB9c (200 OK) 16.6 seconds
(lldb) 

Edited 11/15/17

Request Output
I ran the request in a Paw and I got the following output. As you can see the second element is null. The Spotify API documentation states, "If an object is not found, a null value is returned in the appropriate position. " The track exists in the Spotify library, however audio features for it do not exist. It is possible that Spartan is not properly handling this null value and is searching for an error code that does not exist (because the request was successful) hence the unexpected nil value. Is there a way to get the output of all non null values?

{
  "audio_features": [
    {
      "danceability": 0.880,
      "energy": 0.428,
      "key": 9,
      "loudness": -8.280,
      "mode": 1,
      "speechiness": 0.206,
      "acousticness": 0.149,
      "instrumentalness": 0.0000507,
      "liveness": 0.114,
      "valence": 0.333,
      "tempo": 100.007,
      "type": "audio_features",
      "id": "7sO5G9EABYOXQKNPNiE9NR",
      "uri": "spotify:track:7sO5G9EABYOXQKNPNiE9NR",
      "track_href": "https://api.spotify.com/v1/tracks/7sO5G9EABYOXQKNPNiE9NR",
      "analysis_url": "https://api.spotify.com/v1/audio-analysis/7sO5G9EABYOXQKNPNiE9NR",
      "duration_ms": 172800,
      "time_signature": 4
    },
    null,
    {
      "danceability": 0.797,
      "energy": 0.844,
      "key": 11,
      "loudness": -5.482,
      "mode": 1,
      "speechiness": 0.275,
      "acousticness": 0.0651,
      "instrumentalness": 0,
      "liveness": 0.0870,
      "valence": 0.520,
      "tempo": 170.142,
      "type": "audio_features",
      "id": "4dVpf9jZjcORqGTLUaeYj9",
      "uri": "spotify:track:4dVpf9jZjcORqGTLUaeYj9",
      "track_href": "https://api.spotify.com/v1/tracks/4dVpf9jZjcORqGTLUaeYj9",
      "analysis_url": "https://api.spotify.com/v1/audio-analysis/4dVpf9jZjcORqGTLUaeYj9",
      "duration_ms": 173600,
      "time_signature": 4
    },
    {
      "danceability": 0.837,
      "energy": 0.791,
      "key": 1,
      "loudness": -3.824,
      "mode": 1,
      "speechiness": 0.251,
      "acousticness": 0.0199,
      "instrumentalness": 0,
      "liveness": 0.0836,
      "valence": 0.379,
      "tempo": 176.003,
      "type": "audio_features",
      "id": "2Xqd0wUttjueBfdcltADOv",
      "uri": "spotify:track:2Xqd0wUttjueBfdcltADOv",
      "track_href": "https://api.spotify.com/v1/tracks/2Xqd0wUttjueBfdcltADOv",
      "analysis_url": "https://api.spotify.com/v1/audio-analysis/2Xqd0wUttjueBfdcltADOv",
      "duration_ms": 211006,
      "time_signature": 4
    },
    {
      "danceability": 0.595,
      "energy": 0.431,
      "key": 10,
      "loudness": -7.282,
      "mode": 1,
      "speechiness": 0.486,
      "acousticness": 0.0101,
      "instrumentalness": 0.0000703,
      "liveness": 0.164,
      "valence": 0.107,
      "tempo": 125.200,
      "type": "audio_features",
      "id": "0Gux2yTMWYOlcBUNjGJu5p",
      "uri": "spotify:track:0Gux2yTMWYOlcBUNjGJu5p",
      "track_href": "https://api.spotify.com/v1/tracks/0Gux2yTMWYOlcBUNjGJu5p",
      "analysis_url": "https://api.spotify.com/v1/audio-analysis/0Gux2yTMWYOlcBUNjGJu5p",
      "duration_ms": 227707,
      "time_signature": 3
    },
    {
      "danceability": 0.689,
      "energy": 0.761,
      "key": 1,
      "loudness": -4.939,
      "mode": 1,
      "speechiness": 0.130,
      "acousticness": 0.0485,
      "instrumentalness": 0,
      "liveness": 0.136,
      "valence": 0.698,
      "tempo": 78.514,
      "type": "audio_features",
      "id": "2vjmyyAnDUzilFYxOlTdO1",
      "uri": "spotify:track:2vjmyyAnDUzilFYxOlTdO1",
      "track_href": "https://api.spotify.com/v1/tracks/2vjmyyAnDUzilFYxOlTdO1",
      "analysis_url": "https://api.spotify.com/v1/audio-analysis/2vjmyyAnDUzilFYxOlTdO1",
      "duration_ms": 157643,
      "time_signature": 4
    },
    {
      "danceability": 0.580,
      "energy": 0.531,
      "key": 5,
      "loudness": -6.631,
      "mode": 0,
      "speechiness": 0.0776,
      "acousticness": 0.128,
      "instrumentalness": 0.000127,
      "liveness": 0.143,
      "valence": 0.141,
      "tempo": 159.786,
      "type": "audio_features",
      "id": "1OmcAT5Y8eg5bUPv9qJT4R",
      "uri": "spotify:track:1OmcAT5Y8eg5bUPv9qJT4R",
      "track_href": "https://api.spotify.com/v1/tracks/1OmcAT5Y8eg5bUPv9qJT4R",
      "analysis_url": "https://api.spotify.com/v1/audio-analysis/1OmcAT5Y8eg5bUPv9qJT4R",
      "duration_ms": 218320,
      "time_signature": 4
    },
    {
      "danceability": 0.950,
      "energy": 0.533,
      "key": 5,
      "loudness": -7.022,
      "mode": 1,
      "speechiness": 0.148,
      "acousticness": 0.247,
      "instrumentalness": 0,
      "liveness": 0.114,
      "valence": 0.675,
      "tempo": 119.869,
      "type": "audio_features",
      "id": "6foxplXS0YEq8cO374Lty4",
      "uri": "spotify:track:6foxplXS0YEq8cO374Lty4",
      "track_href": "https://api.spotify.com/v1/tracks/6foxplXS0YEq8cO374Lty4",
      "analysis_url": "https://api.spotify.com/v1/audio-analysis/6foxplXS0YEq8cO374Lty4",
      "duration_ms": 124056,
      "time_signature": 4
    },
    {
      "danceability": 0.782,
      "energy": 0.436,
      "key": 2,
      "loudness": -7.033,
      "mode": 1,
      "speechiness": 0.155,
      "acousticness": 0.331,
      "instrumentalness": 0.0000130,
      "liveness": 0.342,
      "valence": 0.243,
      "tempo": 82.994,
      "type": "audio_features",
      "id": "5u6vkDnOyaf8LsteDAj2ub",
      "uri": "spotify:track:5u6vkDnOyaf8LsteDAj2ub",
      "track_href": "https://api.spotify.com/v1/tracks/5u6vkDnOyaf8LsteDAj2ub",
      "analysis_url": "https://api.spotify.com/v1/audio-analysis/5u6vkDnOyaf8LsteDAj2ub",
      "duration_ms": 268933,
      "time_signature": 4
    },
    {
      "danceability": 0.890,
      "energy": 0.633,
      "key": 11,
      "loudness": -5.475,
      "mode": 1,
      "speechiness": 0.168,
      "acousticness": 0.0232,
      "instrumentalness": 0.000343,
      "liveness": 0.0993,
      "valence": 0.425,
      "tempo": 139.948,
      "type": "audio_features",
      "id": "1XRgIKC5TPwo7nWGyKqgG0",
      "uri": "spotify:track:1XRgIKC5TPwo7nWGyKqgG0",
      "track_href": "https://api.spotify.com/v1/tracks/1XRgIKC5TPwo7nWGyKqgG0",
      "analysis_url": "https://api.spotify.com/v1/audio-analysis/1XRgIKC5TPwo7nWGyKqgG0",
      "duration_ms": 233087,
      "time_signature": 4
    },
    {
      "danceability": 0.577,
      "energy": 0.593,
      "key": 9,
      "loudness": -4.816,
      "mode": 0,
      "speechiness": 0.137,
      "acousticness": 0.800,
      "instrumentalness": 0,
      "liveness": 0.623,
      "valence": 0.575,
      "tempo": 120.611,
      "type": "audio_features",
      "id": "7zLGHiDWd9T1Rxw4PQCb13",
      "uri": "spotify:track:7zLGHiDWd9T1Rxw4PQCb13",
      "track_href": "https://api.spotify.com/v1/tracks/7zLGHiDWd9T1Rxw4PQCb13",
      "analysis_url": "https://api.spotify.com/v1/audio-analysis/7zLGHiDWd9T1Rxw4PQCb13",
      "duration_ms": 303781,
      "time_signature": 5
    },
    {
      "danceability": 0.901,
      "energy": 0.529,
      "key": 2,
      "loudness": -4.443,
      "mode": 1,
      "speechiness": 0.191,
      "acousticness": 0.0519,
      "instrumentalness": 0,
      "liveness": 0.179,
      "valence": 0.146,
      "tempo": 138.003,
      "type": "audio_features",
      "id": "1YZfcVLbbJwfizR5cDOp3q",
      "uri": "spotify:track:1YZfcVLbbJwfizR5cDOp3q",
      "track_href": "https://api.spotify.com/v1/tracks/1YZfcVLbbJwfizR5cDOp3q",
      "analysis_url": "https://api.spotify.com/v1/audio-analysis/1YZfcVLbbJwfizR5cDOp3q",
      "duration_ms": 307660,
      "time_signature": 4
    },
    {
      "danceability": 0.860,
      "energy": 0.561,
      "key": 6,
      "loudness": -5.757,
      "mode": 0,
      "speechiness": 0.185,
      "acousticness": 0.274,
      "instrumentalness": 0,
      "liveness": 0.108,
      "valence": 0.354,
      "tempo": 152.984,
      "type": "audio_features",
      "id": "7r6LNJT2LqpLpEyZQJPygt",
      "uri": "spotify:track:7r6LNJT2LqpLpEyZQJPygt",
      "track_href": "https://api.spotify.com/v1/tracks/7r6LNJT2LqpLpEyZQJPygt",
      "analysis_url": "https://api.spotify.com/v1/audio-analysis/7r6LNJT2LqpLpEyZQJPygt",
      "duration_ms": 189480,
      "time_signature": 4
    },
    {
      "danceability": 0.658,
      "energy": 0.736,
      "key": 9,
      "loudness": -6.097,
      "mode": 0,
      "speechiness": 0.308,
      "acousticness": 0.113,
      "instrumentalness": 0,
      "liveness": 0.0978,
      "valence": 0.804,
      "tempo": 143.966,
      "type": "audio_features",
      "id": "57IRaiAB4hBZu3gnNVZC0v",
      "uri": "spotify:track:57IRaiAB4hBZu3gnNVZC0v",
      "track_href": "https://api.spotify.com/v1/tracks/57IRaiAB4hBZu3gnNVZC0v",
      "analysis_url": "https://api.spotify.com/v1/audio-analysis/57IRaiAB4hBZu3gnNVZC0v",
      "duration_ms": 160000,
      "time_signature": 4
    },
    {
      "danceability": 0.749,
      "energy": 0.671,
      "key": 6,
      "loudness": -5.151,
      "mode": 1,
      "speechiness": 0.0599,
      "acousticness": 0.680,
      "instrumentalness": 0,
      "liveness": 0.167,
      "valence": 0.756,
      "tempo": 81.994,
      "type": "audio_features",
      "id": "6GNifiuBPrKFpwNBYnooFm",
      "uri": "spotify:track:6GNifiuBPrKFpwNBYnooFm",
      "track_href": "https://api.spotify.com/v1/tracks/6GNifiuBPrKFpwNBYnooFm",
      "analysis_url": "https://api.spotify.com/v1/audio-analysis/6GNifiuBPrKFpwNBYnooFm",
      "duration_ms": 268864,
      "time_signature": 4
    },
    {
      "danceability": 0.921,
      "energy": 0.467,
      "key": 1,
      "loudness": -8.443,
      "mode": 1,
      "speechiness": 0.119,
      "acousticness": 0.0149,
      "instrumentalness": 0.000238,
      "liveness": 0.334,
      "valence": 0.287,
      "tempo": 135.995,
      "type": "audio_features",
      "id": "3ncgNpxLoBQ65ABk4djDyd",
      "uri": "spotify:track:3ncgNpxLoBQ65ABk4djDyd",
      "track_href": "https://api.spotify.com/v1/tracks/3ncgNpxLoBQ65ABk4djDyd",
      "analysis_url": "https://api.spotify.com/v1/audio-analysis/3ncgNpxLoBQ65ABk4djDyd",
      "duration_ms": 191252,
      "time_signature": 4
    },
    {
      "danceability": 0.717,
      "energy": 0.753,
      "key": 7,
      "loudness": -4.609,
      "mode": 0,
      "speechiness": 0.311,
      "acousticness": 0.293,
      "instrumentalness": 0,
      "liveness": 0.158,
      "valence": 0.631,
      "tempo": 169.857,
      "type": "audio_features",
      "id": "2vaMWMPMgsWX4fwJiKmdWm",
      "uri": "spotify:track:2vaMWMPMgsWX4fwJiKmdWm",
      "track_href": "https://api.spotify.com/v1/tracks/2vaMWMPMgsWX4fwJiKmdWm",
      "analysis_url": "https://api.spotify.com/v1/audio-analysis/2vaMWMPMgsWX4fwJiKmdWm",
      "duration_ms": 174012,
      "time_signature": 4
    },
    {
      "danceability": 0.721,
      "energy": 0.710,
      "key": 7,
      "loudness": -9.294,
      "mode": 1,
      "speechiness": 0.295,
      "acousticness": 0.000655,
      "instrumentalness": 0,
      "liveness": 0.359,
      "valence": 0.509,
      "tempo": 108.024,
      "type": "audio_features",
      "id": "0taOCiup4HNG9LmbduVlJj",
      "uri": "spotify:track:0taOCiup4HNG9LmbduVlJj",
      "track_href": "https://api.spotify.com/v1/tracks/0taOCiup4HNG9LmbduVlJj",
      "analysis_url": "https://api.spotify.com/v1/audio-analysis/0taOCiup4HNG9LmbduVlJj",
      "duration_ms": 156057,
      "time_signature": 5
    },
    {
      "danceability": 0.785,
      "energy": 0.622,
      "key": 8,
      "loudness": -6.741,
      "mode": 1,
      "speechiness": 0.254,
      "acousticness": 0.0133,
      "instrumentalness": 0,
      "liveness": 0.154,
      "valence": 0.466,
      "tempo": 78.475,
      "type": "audio_features",
      "id": "59J5nzL1KniFHnU120dQzt",
      "uri": "spotify:track:59J5nzL1KniFHnU120dQzt",
      "track_href": "https://api.spotify.com/v1/tracks/59J5nzL1KniFHnU120dQzt",
      "analysis_url": "https://api.spotify.com/v1/audio-analysis/59J5nzL1KniFHnU120dQzt",
      "duration_ms": 235535,
      "time_signature": 4
    },
    {
      "danceability": 0.930,
      "energy": 0.673,
      "key": 10,
      "loudness": -7.314,
      "mode": 0,
      "speechiness": 0.0733,
      "acousticness": 0.0112,
      "instrumentalness": 0,
      "liveness": 0.138,
      "valence": 0.219,
      "tempo": 134.984,
      "type": "audio_features",
      "id": "4EsYkJjHKMejYLp54woB9c",
      "uri": "spotify:track:4EsYkJjHKMejYLp54woB9c",
      "track_href": "https://api.spotify.com/v1/tracks/4EsYkJjHKMejYLp54woB9c",
      "analysis_url": "https://api.spotify.com/v1/audio-analysis/4EsYkJjHKMejYLp54woB9c",
      "duration_ms": 188563,
      "time_signature": 4
    }
  ]
}

How to get a Playlist object?

I want to access the 'spotifyDescription' of Playlist, however as I searched through the methods, the 'Playlist' class is not even used anywhere.

Get track id from Paging Object.

I am wondering if there is a way to get the track id from a Paging Object. Here is the method I am using:

playSpotifyURI("spotify:track:" + track, startingWith: 0, startingWithPosition: 0)

This works if track is 58s6EuEYJdlb0kO7awm3Vp

I use this to get an array of tracks:

Spartan.search(query: search, type: .playlist, success:...

This returns a Paging Object which I get the items array from. The items array contains an href which looks like this https://api.spotify.com/v1/users/spotify/playlists/37i9dQZF1DX4y8h9WqDPAE/tracks. Unfortunately this does not contain the exact track id, it only gives the playlist id.

Using `getArtists` method causes crash

I was using the getArtists(ids: [String]...) method with chunks of artist identifiers, each counting 50, as API states that's the limit and in some cases the app crashes. See attached screenshot.

Zrzut ekranu 2020-04-12 o 10 38 30

The crash is only happening with some artists, as users reported. Not all of them experienced the crash.

Search: pagingObject.canMakeNextRequest condition does not work

Hi,
Yesterday the pagingObject.canMakeNextRequest condition stopped working as expected.

It does end at offset=850 but according to the Spotify Web API Console there should be a total of 95902 results.

If I remove the canMakeNextRequest condition and call searchNewAlbums regardless of this value, it runs to the 10000 offset limit just fine.

Here is the code snippet:

 _ = Spartan.search(query: "tag:new", type: .album, market: .de, limit: 50, offset: offset, success: { (pagingObject: PagingObject<SimplifiedAlbum>) in
            // temporary workaround for null objects in search results
            if (pagingObject.items != nil){
                for album in pagingObject.items {
                        self.releasesIdSet.insert(album.id as! String)
                }
                
                if pagingObject.canMakeNextRequest {
                    self.searchNewAlbums(offset: (offset+pagingObject.limit))
                }
            } else {
                print("PAGING OBJECT == NIL")
                // TODO do error handling
            }
        }, failure: { (error) in
            print(error)
            // TODO do error handling
        })

The last get request until it finishes and there is no error message:
๐Ÿ”ต [AlamoRecordLogger] GET https://api.spotify.com/v1/search?limit=50&market=DE&offset=850&q=tag%3Anew&type=album

And a screenshot from the Web API Console:
bildschirmfoto 2018-10-20 um 09 40 19

getNext on pagingObject for PlaylistTracks failing

Thank you for creating such an awesome Spotify Client! There's a small issue with getNext being called on a pagingObject dealing with tracks fetched for a playlist. Spartan.getPlaylistTracks() works fine and loads 20 tracks. However after retrieving the pagingObject and calling getNext on it I get fatal error: unexpectedly found nil while unwrapping an Optional value 2017-07-09 19:55:16.220157-0500 Sink[12993:3968613] fatal error: unexpectedly found nil while unwrapping an Optional value. To confirm I called getNext right after fetching the pagingObject as follows ` let _ = Spartan.getPlaylistTracks(userId: userId, playlistId: playlistId, success: { (tracksPage) in

        if tracksPage.canMakeNextRequest {
            tracksPage.getNext(success: { (trackpage) in
                print("successfully fetched \(trackpage)")
            }, failure: { (error) in
                print(error.description)
                print(error.errorMessage)
            })
        }
         //completionHandler(tracksPage)
    }) { (error) in
        print(error.description)
        print(error.errorMessage)
    }

` and I get the error. I've also attached the stack trace below.

I am quite certain you'd be able to recreate the error if you fetch tracks for playlist that has number of songs over the the limit of 1 pagingObject and calling getNext on the pagingObject. Any fix or direction would be greatly appreciated!

screen shot 2017-07-09 at 7 56 24 pm

`getTrack` returns invalid Track object whereas `getTracks` works as expected

When using Spartan method getTrack(id: String...) Track object is received but most of its properties are nil, when I checked the uri of the object it turns out that actually an Album object was received but when Spartan tried to map it to Track most of the properties were nil.

See the Xcode Quicklook of the object
Zrzut ekranu 2020-04-13 o 14 06 43

You can see that the object is in fact an album.
I think I'm calling the method correctly

Zrzut ekranu 2020-04-13 o 14 07 24

Although this method gives wrong results, using getTracks(ids: [String]...) works correctly and I'll be using it to get single tracks from the API

SpartanError: Insufficient client scope

I'm trying to create a playlist for the current user and received the error "SpartanError: Insufficient client scope". I have a valid auth token that I refresh and can get the current user's info fine. The scopes I have selected are:
static let SCOPE = ["user-read-email", "user-top-read", "playlist-modify-public", "playlist-modify-private"]

My Code for token request:

static func accessCodeToAccessToken(code: String, completion: @escaping (Result<Tokens, Error>) -> Void) -> Request {
        
        Request.buildRequest(method: .post,
                             header: Header.POSTHeader.buildHeader(),
                             baseURL: SpotifyBaseURL.authBaseURL.rawValue,
                             path: EndingPath.token.buildPath(),
                             params: Parameters.codeForToken(accessCode: code).buildParameters()) { (result) in
            
            result.decoding(Tokens.self, completion: completion)
        }
    }

My code calling request in view controller:

private func getSpotifyAccessCode() {

        let urlRequest = client.getSpotifyAccessCodeURL()
        print(urlRequest)
        let scheme = "auth"
        let session = ASWebAuthenticationSession(url: urlRequest, callbackURLScheme: scheme) { (callbackURL, error) in
            guard error == nil, let callbackURL = callbackURL else { return }

            let queryItems = URLComponents(string: callbackURL.absoluteString)?.queryItems
            guard let requestAccessCode = queryItems?.first(where: { $0.name == "code" })?.value else { return }
            print(" Code \(requestAccessCode)")
            // exchanges access code to get access token and refresh token
            self.client.call(request: .accessCodeToAccessToken(code: requestAccessCode, completion: { (token) in
                switch token {

                case .failure(let error):
                    print("ERROR: ", error)
                case .success(let token):
                    UserDefaults.standard.set(token.accessToken, forKey: "token")
                    UserDefaults.standard.set(token.refreshToken, forKey: "refresh_token")
                    print("SUCCESS")
                    
                    DispatchQueue.main.async {
                        self.goToApp()
                    }
                }
            }))

        }

        session.presentationContextProvider = self
        session.start()

    }

Create playlist code:

 func createPlaylist(user: UserModel) {
        
        let name = "Test"
        
        let createPlaylist = Spartan.createPlaylist(userId: user.id, name: name, isPublic: true) { (playlist) in
            print("PLAYLIST CREATED")
            
        } failure: { (error) in
            print("Error creating playlist: ", error)
        }

    }

Sorry if this isn't enough information please let me know.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.