Business central blog
Subscribe to blog and get news about new posts.

Spotify to Business Central integration.

Continuing my cycle of posts on basic examples of integrating third-party applications with BC, I decided to choose Spotify API for demonstration.
Spotify is a Swedish-based audio streaming and media services provider. It is a pretty popular service and has a very powerful free API with detailed documentation.
In the last post, I wrote about basic work with JSON and GET http request using the New York TImes API as an example. Now I want to delve deeper into this topic, analyze the POST / PUT / DELETE methods and create a JSON structure for sending to the server. We will also cover OAuth 2.0 and page switching.

OAuth 2.0

I originally thought of writing my own OAuth 2.0 authentication module, but I found a very high-quality generic module from Msn Raju. Since it is distributed under the MIT license, I recommend it for use for any third-party application authentication. At the same time, I want to recommend the blog of the author of this module, this is high-quality content that will be of interest to any BC developer. I also recommend that you read the article about OAuth 2.0 on his blog.

Since this generic module fully meets my needs for integrating Spotify to Business Central, I just need to install and configure it, and my demo project will have dependices for this application. The dependencies diagram of these two extensions looks like this:
You can read about how OAuth 2.0 authorization works on Spotify API documentation:
https://developer.spotify.com/documentation/general/guides/authorization-guide/

The OAuth 2.0 diagram can also be found on the Spotify Authorization Guide.
First of all, we have to register at https://www.spotify.com/ after that we need to register the integration application on the Spotify dashboard https://developer.spotify.com/dashboard/applications.
After creation, we can see the Client Id and Client Secret of our application, as well as statistics.
Next, it is very important to add a Redirect URL, you can do this by changing the application settings on Spotify Application Dashboard. I am working on my bc Sandbox, so URL should looking like this: https://businesscentral.dynamics.com/%tenant_id%/%sandbox_name%/OAuthLanding.html.
OAuthLanding.html is a default redirect page from the OAuth 2.0 Msn Raju module.
With this information, we can already proceed with obtaining Spotify Access Token. Install the OAuth 2.0 module and configure it for Spotify. Find OAuth 2.0 Applications page and create a new record, fill important fields:
Client Id - your client id from Spotify.
Client Secret - your client secret from Spotify.
Grant Type - Authorization Code
Redirect URL - https://businesscentral.dynamics.com/%tenant_id%/%sandbox_name%/OAuthLanding.html
Scope - for demo we can use these scopes: playlist-modify-private%20playlist-read-collaborative%20playlist-read-private%20user-read-private%20playlist-modify-public%20user-modify-playback-state%20user-top-read
Authorization URL - https://accounts.spotify.com/authorize
Access Token URL - https://accounts.spotify.com/api/token
Now all that remains is to press the Request Access Token action.

Spotify - BC Application

After successfully obtaining the Spotify Access Token, you need to install Spotify - BC Application, I have posted it on GitHub. Next, look for the Spotify General Setup page and fill in two fields:
Base API URL - https://api.spotify.com/v1/
Spotify OAuth settings - link to code which we create before in OAuth 2.0 section, in my case it is SPOTIFY.

Supported features

Playlist managing
Load, create and rename your own playlists from Business Central.
Search tracks
Search tracks in Spotify from Business Central (careful, it can take a long time).
Adding tracks to playlist
Add founded tracks to your playlist.
Show or delete tracks from playlist
Show or delete tracks from playlist.

POST/PUT/DELETE http requests in AL

Based on the documentation https://developer.spotify.com/documentation/web-api/reference/, we can observe that some API requests must be sent with the POST / PUT / DELETE http methods and sometimes contain content to be sent.
For this, I created a generic function that can work with any of the methods:
procedure SendRequest(contentToSend: Text; RequestMethod: Text; AdditionalURL: Text): text
    var
        SpotifyGeneralSetup: Record "SBC Spotify General Setup";
        client: HttpClient;
        request: HttpRequestMessage;
        response: HttpResponseMessage;
        contentHeaders: HttpHeaders;
        content: HttpContent;
        requestUri: Text;
        responseText: Text;
        errorBodyContent: Text;
    begin
        SpotifyGeneralSetup.get();
        SpotifyGeneralSetup.TestField("Spotify API URL");

        requestUri := SpotifyGeneralSetup."Spotify API URL" + AdditionalURL;

        content.WriteFrom(contentToSend);

        content.GetHeaders(contentHeaders);
        contentHeaders.Clear();
        contentHeaders.Add('Content-Type', 'application/json');

        request.Content := content;

        request.SetRequestUri(requestUri);
        request.Method := RequestMethod;

        client.DefaultRequestHeaders().Add(
                'Authorization',
                'Bearer ' + GetSpotifyAccessToken());
        client.Send(request, response);

        response.Content().ReadAs(responseText);
        if not response.IsSuccessStatusCode() then begin
            response.Content().ReadAs(errorBodyContent);
            Error(RequestErr, response.HttpStatusCode(), errorBodyContent);
        end;

        exit(responseText);
    end;
Also, we should look at the function which returns our Access Token from OAuth 2.0 generic module GetSpotifyAccessToken():
procedure GetSpotifyAccessToken(): Text
    var
        SpotifyGeneralSetup: Record "SBC Spotify General Setup";
        OAuth20Appln: Record "OAuth 2.0 Application";
        OAuth20AppHelper: Codeunit "OAuth 2.0 App. Helper";
        MessageText: Text;
    begin
        SpotifyGeneralSetup.Get();
        SpotifyGeneralSetup.TestField("Spotify OAuth settings");
        OAuth20Appln.Get(SpotifyGeneralSetup."Spotify OAuth settings");
        if not OAuth20AppHelper.RequestAccessToken(OAuth20Appln, MessageText) then
            Error(MessageText);

        exit(OAuth20AppHelper.GetAccessToken(OAuth20Appln));
    end;
Let's look at an example of how you can use the SendRequest function to change the playlist. The Spotify API documentation contains information that to change information about a playlist, we need a request with the PUT method, to the endpoint https://api.spotify.com/v1/playlists/{playlist_id} with content in JSON format that we are going to change.
https://developer.spotify.com/documentation/web-api/reference/playlists/change-playlist-details/
If we want to change the name of our playlist to 'My Best Music' then the JSON that we send should look like this:
{
  "name": "My Best Music"
}
If we combine information, then we can write a function to change information about the playlist:
procedure ModifyPlaylist(EntityName: Text; EntityValue: Text; PlaylistId: Text)
    var
        SBCSpotifyAPIMgt: Codeunit "SBC Spotify API Mgt";
        contentToSend: Text;
        JObject: JsonObject;
    begin
        JObject.Add(EntityName, EntityValue);
        JObject.WriteTo(contentToSend);
        SBCSpotifyAPIMgt.SendRequest(contentToSend, 'PUT', StrSubstNo('playlists/%1', PlaylistId));
    end;
Here is an example of its use on Name OnValidate trigger:
ModifyPlaylist(LowerCase(Rec.FieldName(Name)), Rec.Name, Rec.Id);

JSON page switching

Spotify APIs, like many others, have a limit on the size of the JSON returned. Different endpoints can have different record limits per one page. For example, the maximum limit per page for getting playlists is 50.
https://developer.spotify.com/documentation/web-api/reference/playlists/get-a-list-of-current-users-playlists/
This means that our http request will return a limited amount of information in JSON format and we need to make a new request, but for the next records. Spotify supports this with two parameters:
Limit - the maximum number of playlists to return.
Offset - the index of the first playlist to return. Default: 0 (the first object). Maximum offset: 100000.
For example, if we have 120 playlists, and the limit is 50, then in to get all the playlists we need to make three requests:
1. https://api.spotify.com/v1/me/playlists?limit=50
2. https://api.spotify.com/v1/me/playlists?limit=50&offset=50
3. https://api.spotify.com/v1/me/playlists?limit=50&offset=100
It is noteworthy that the Spotify API stores the full link to the next request in the JSON returned. But since this is not always the case, I decided to ignore this fact and write a function to get a specific offset:
procedure GetNextPageID(Data: Text; PageScope: Text): Integer
    var
        JToken: JsonToken;
        JToken2: JsonToken;
        JObject: JsonObject;
        ExitValue: Integer;
    begin
        if Data = '' then
            exit;

        JToken.ReadFrom(Data);
        JObject := JToken.AsObject();
        if PageScope <> '' then begin
            JObject.SelectToken(PageScope, JToken2);
            JObject := JToken2.AsObject();
        end;
        JObject.Get('next', JToken2);


        if JToken2.AsValue().IsNull() then
            ExitValue := 0
        else begin
            JObject.Get('limit', JToken2);
            ExitValue := JToken2.AsValue().AsInteger();
            JObject.Get('offset', JToken2);
            ExitValue += JToken2.AsValue().AsInteger();
        end;

        exit(ExitValue);
    end;
procedure GetMyPlaylists()
    var
        SBCJSONMgt: Codeunit "SBC JSON Mgt";
        NextPageID: Integer;
        NextPageExist: Boolean;
        AddUrl: Text;
        Data: Text;
        HttpStatusCode: Integer;
    begin
        NextPageExist := true;
        while NextPageExist do begin
            AddUrl := StrSubstNo('me/playlists?limit=50&offset=%1', NextPageID);
            if GetRequest(AddUrl, Data, HttpStatusCode, '', NextPageID) then
                SBCJSONMgt.UpdateMyPlaylists(Data);
            NextPageExist := NextPageID <> 0;
        end;
    end;

Source code

SUNDAY, November 15, 2020