Automating Spotify playlist creation and updating using the Spotify API - 2023-01-31
Using the Spotify API to add / remove music to an existing playlist.
Although Spotify does a great job at providing you with pre-made music selections and playlists based off artist and genres you like. I found myself having to go through multiple playlist for artist I remembered (I know I should have liked the song when I had the chance), which became annoying because Spotify would then create more playlists.
So I thought to myself, why don't I just get all the artists in specific genres and find their new music myself! No more randomly playing through these preset playlists.
Luckily for me other people probably thought the same thing and I found the perfect open source library to help me out, SpotifyAPI-NET.
Additionally, I decided that I wanted this application to run on a schedule, so I opted to build a Windows Service using a BackgroundService application.
Check out all the documentation sites below:
Table of Contents
Disclaimer 1: You should always write tests, I am still working to get better at that so for this demoI didn't include them.
Disclaimer 2: There are plenty of ways to do this, this is my v1
Spotify Developer Dashboard
If you haven't already, create an account and create an app in the Spotify Developer Dashboard
Developer Spotify SiteTake note of your Client ID, which we will use later
Worker Service Project with Visual Studio
Using the .NET Cli, I used the following command to set up my project. I just followed the example documentation and was setup in minutes.
dotnet new worker --name <Project.Name>
Install the SpotifyAPI-NET packages.
dotnet add package SpotifyAPI.Web
# Optional Auth module, which includes an embedded HTTP Server for OAuth2
dotnet add package SpotifyAPI.Web.Auth
Spotify API Authentication and Access Token
After reviewing their example documentation, I was able to generate a bearer token and start using the API to call data. I quickly found out that I didn't have enough access to start manipulating a playlist. (Be sure to read the documentation!)
So I found that I needed to use the Authorization code with PKCE flow. This enables you request the correct authorization, with the correct scopes to access all your data.
Authorization code with PKCEWorker.cs
This is the main entry point of our application. So I opted to do the login process here.
We start with our private variables
_logger: A generic interface for logging.
_config: Represents a set of key/value application configuration properties.
_server: A small embedded Web Server for the code retrieval. See the Using Spotify.Web.Auth section in the API documentation.
CredentialsPath: path to file that contains and stores current access token.
private readonly ILogger<Worker> _logger;
private IConfiguration _config;
private static readonly EmbedIOAuthServer _server = new EmbedIOAuthServer(new Uri("http://localhost:5000/callback"), 5000);
private const string CredentialsPath = "credentials.json";
ExecuteAsync Method
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
string clientId = _config["ClientID"];
if (string.IsNullOrEmpty(clientId))
{
throw new NullReferenceException(
"Please set SPOTIFY_CLIENT_ID via environment variables before starting the program"
);
}
if (File.Exists(CredentialsPath))
{
await Start(clientId);
}
else
{
await StartAuthentication(clientId);
}
}
This is the main entry point to the application.
Here we get the ClientID from the configuration file, verify its not null.
Then check to see if we have current credentials.
Finally, if there are credentials the application runs, if not, we get new credentials.
Authentication - StartAuthentication Method
This method accepts the clientId we got from the configuration file.
We then create two variables that get populated from the PKCEUtil class.
We use the _server private global variable to start the server, which will assist us with executing the request.
The application then checks to if a authorization code was received. If it has, it stops the server, and creates a PKCE Token object and writes it to the credentials file for storage and use. Then kicks of the Start Method which runs the application.
If a token new token is required, the application then creates a request object to send. This request object contains all the proper scopes needed to modify objects in the Spotify API. In my case, I added all the playlist related scopes.
The application then executes and logs you in. The very first instance will prompt you for your Spotify credentials.
private async Task StartAuthentication(string clientId)
{
var (verifier, challenge) = PKCEUtil.GenerateCodes();
await _server.Start();
_server.AuthorizationCodeReceived += async (sender, response) =>
{
await _server.Stop();
PKCETokenResponse token = await new OAuthClient().RequestToken(
new PKCETokenRequest(clientId!, response.Code, _server.BaseUri, verifier)
);
await File.WriteAllTextAsync(CredentialsPath, JsonConvert.SerializeObject(token));
await Start(clientId);
};
var request = new LoginRequest(_server.BaseUri, clientId!, LoginRequest.ResponseType.Code)
{
CodeChallenge = challenge,
CodeChallengeMethod = "S256",
Scope = new List<string> {
UserReadEmail,
UserReadPrivate,
PlaylistReadPrivate,
PlaylistReadCollaborative,
PlaylistModifyPrivate,
PlaylistModifyPublic,
}
};
Uri uri = request.ToUri();
try
{
BrowserUtil.Open(uri);
}
catch (Exception)
{
Console.WriteLine("Unable to open URL, manually open: {0}", uri);
}
}
private async Task Start(string clientId)
{
var json = await File.ReadAllTextAsync(CredentialsPath);
var token = JsonConvert.DeserializeObject<PKCETokenResponse>(json);
var authenticator = new PKCEAuthenticator(clientId!, token!);
authenticator.TokenRefreshed += (sender, token) => File.WriteAllText(CredentialsPath, JsonConvert.SerializeObject(token));
var config = SpotifyClientConfig.CreateDefault()
.WithAuthenticator(authenticator);
var spotify = new SpotifyClient(config);
var me = await spotify.UserProfile.Current();
Console.WriteLine($"Welcome {me.DisplayName} ({me.Id}), you're authenticated!");
SpotifyBot spotifyBot = new SpotifyBot(_logger, spotify);
await spotifyBot.Run();
_server.Dispose();
Environment.Exit(0);
}
Start Method
Again, this method takes in the clientId.
Then the method reads the credentials file and converts the string from that file into a token object.
We then create a PKCE Authenticator variable and pass the clientId and token to it. Which then refreshes the token if needed.
Next, we create the SpotifyClientConfig object and pass the authenticator variable to it. This config object is used to pass to the SpotifyClient object and logs us in!
For demo and verification purposes, I print my user info to the screen.
Finally, I run my application code! Once its complete I close the server.
My thought process and workflow to generate the playlist
After poking around the API and seeing what data I can pull from, I decided that the application was going to flow like this:
Delete all songs in the playlist every time the application runs.
In the future I may get all the songs in order to compare to any new songs, but for now it was an unnecessary API call.
Get Artists from a specified Genre
Iterate through the artists to check if they need to be excluded for the following reasons:
Hits on an excluded Artist name. A list of hard coded artists names is maintained in a private variable.
Hits on an artist name already processed. An list of artists names processed is maintained in a private variable.
Artist popularity rating is greater than 15.
If the artist meets all that criteria, get and iterate through the albums to check if they need to be excluded for the following reasons:
Hits on an excluded Album name. A list of hard coded album names is maintained in a private variable.
Hits on an album name already processed. An list of album names processed is maintained in a private variable.
If the album release date falls outside of the date range of 21 days.
If the album fall within the date range of 21 days, I get the tracks of that specific album
I then iterate through the tracks, and store the song URI property in two list, one to pass to add to the playlist and another to track what songs are currently being added so we don't add them multiple times.
We the add the songs to playlist.
Spotify API Endpoints / SpotifyAPI-NET Interfaces
Below are all the endpoints I used to accomplish my workflow. Be sure to checkout the Spotify Console Site to check out what gets turned for each object.
Get Playlist - https://api.spotify.com/v1/playlists/{playlist_id}/tracks
Using the SpotifyAPI-NET code, get the playlist object
var playlist = await _spotify.Playlists.Get(_playlistID);
Delete Tracks from Playlist - https://api.spotify.com/v1/playlists/{playlist_id}/tracks
Using the SpotifyAPI-NET code, build the remove items request object.
PlaylistRemoveItemsRequest request = new PlaylistRemoveItemsRequest(); request.Tracks = playlistIds; var delete = await _spotify.Playlists.RemoveItems(_playlistID, request);
Search for Item - https://api.spotify.com/v1/search
Using the SpotifyAPI-NET code, get all artists in a specific genre.
var searchRequest = new SearchRequest(SearchRequest.Types.All, genre); var searchResults = await _spotify.Search.Item(searchRequest);
Get Artists Albums - https://api.spotify.com/v1/artists/{id}/albums
Using the SpotifyAPI-NET code, get all artists albums.
var searchResults = await _spotify.Artists.GetAlbums(artist.Id);
Get All Album Trackshttps://api.spotify.com/v1/albums/{id}/tracks
Using the SpotifyAPI-NET code, get all albums tracks.
var searchResults = await _spotify.Albums.GetTracks(album.Id);
Add track URIs to the playlisthttps://api.spotify.com/v1/playlists/{playlist_id}/tracks
Using the SpotifyAPI-NET code, adding track uris to the selected playlist.
PlaylistAddItemsRequest request = new PlaylistAddItemsRequest(trackURIs); var update = await _spotify.Playlists.AddItems(_playlistID, request);
Issues I came across
While developing and testing the API capabilities I did encounter some issues / gotchas that are worth noting.
Pagination: In order to iterate through all results / delete all the songs in a playlist I had to use the Pagination functionality.
await foreach (var artist in _spotify.Paginate(searchResults.Artists, (s) => s.Artists)){...}
API request limit. During testing I would hit the limit on 2-3 runs which would time my app out for about 20 hours. So I recommend really focusing on working with paged results.
I found that I was getting random artists, albums and tracks. I am still investigating why they hit on my criteria, but this is why I decided to incorporate exclusion lists for now.
Conclusion
This was a fun exercise working with what I would say a well established API and library. The API gives us a lot of data to work with, and the library (once you understand it) gives easy access to the endpoints and what is required for specific objects.
The application workflow isn't perfect, and I plan on refining it with time but for now I am happy with the results and the music.
I do have some hard coded variables like the exclusion lists and playlistID but this wasn't supposed to be UI application where you can select options.
I plan on deploying this as Windows Service and having it run every 21 days.
Happy Coding!
