Trying to scrobble track to last.fm yields perpetual error 13: Invalid method signature supplied

36 Views Asked by At

I am having a frustrating experience trying to scrobble two test tracks to the last.fm API service, using C#.

I have spent almost two days searching Google for solutions, and trying to implement the ideas gleaned from my research. However, I consistently receive an error 13.

The method signature is an MD5 hash of the sequence of parameters that must be concatenated in alphabetical order and UTF8-encoded. More information in section 8 (end of page) of this page: https://www.last.fm/api/authspec

I am using RestSharp to make the API calls. This is how I initialize it:

    // Initialize REST client.
    var options = new RestClientOptions(API_ROOT)
    {
        CachePolicy = new CacheControlHeaderValue { NoCache = false },
        UserAgent = USER_AGENT,
        Encoding = Encoding.UTF8,
        ThrowOnDeserializationError = true
    };
    _client = new RestClient(options);

Here is the code that is responsible for preparing the query and for making the API request:

        // Base parameter list.
        var queryParameters = new Dictionary<string, string>()
        {
            { "method", "track.scrobble" },
            { "api_key", _apiKeySig.ApiKey },
            { "sk", _apiKeySig.SessionKey }
        };

        var currentNdx = 0;

        // Process the tracks.
        foreach (var scrobbleTrack in scrobbleTracksList)
        {
            var arrayIndex = currentNdx.ToString();
            currentNdx++;

            scrobbleTrack.ScrobbleStatus = ScrobbleStatus.BeingScrobbled;

            var track = scrobbleTrack.PlayHistoryItem.Track;
            var artist = track.Artists.FirstOrDefault();
            var albumArtist = track.Album.Artists.FirstOrDefault();
            queryParameters.Add($"artist[{arrayIndex}]", artist?.Name ?? string.Empty);
            var albumTitle = ScrobbleService.RemoveTrackTitleLitter(track.Name);
            queryParameters.Add($"track[{arrayIndex}]", albumTitle);
            var albumName = ScrobbleService.RemoveAlbumTitleLitter(track.Album.Name);
            queryParameters.Add($"album[{arrayIndex}]", albumName);
            queryParameters.Add($"albumArtist[{arrayIndex}]", albumArtist?.Name ?? string.Empty);
            queryParameters.Add($"timestamp[{arrayIndex}]", DateTimeService.GetUnixTimestamp(scrobbleTrack.PlayHistoryItem.PlayedAt));
        }

        // Submit batch of scrobbles.

        const Method method = Method.Post;

        var request = new RestRequest
        {
            Method = method,
        };
        request.AddHeader("Content-Type", "application/x-www-form-urlencoded");

        var query = GenerateQuery(queryParameters, false);
        request.AddBody(query);

        var response = await _client.ExecuteAsync(request, ct);
        if (response.IsSuccessful)
        {
            // DEBUG
            _logLine.LogLine(response.Content ?? "[null]");

            // Update status of all tracks that were scrobbled.
            foreach (var scrobbleTrack in scrobbleTracksList)
            {
                if (scrobbleTrack.ScrobbleStatus != ScrobbleStatus.BeingScrobbled)
                {
                    continue;
                }

                scrobbleTrack.ScrobbleStatus = ScrobbleStatus.HasBeenScrobbled;
            }
        }

        ct.ThrowIfCancellationRequested();

        // More code and optimizations once the call to track.scrobble call is actually successful.

Here are the definitions of the GenerateQuery and SignedCall methods:

    private static string GenerateQuery(Dictionary<string, string> queryParameters, bool jsonFormat = true)
    {
        // LOCATION 1
        var json = JsonConvert.SerializeObject(queryParameters, Formatting.Indented);

        // SignedCall method also does the MD5 hashing.
        queryParameters.Add("api_sig", SignedCall(queryParameters));

        if (jsonFormat)
        {
            queryParameters.Add("format", "json");
        }

        var queryString = string.Join("&", queryParameters
            .Select(kvp => $"{kvp.Key}={HttpUtility.UrlEncode(kvp.Value)}"));

        // LOCATION 5

        return queryString;
    }

    private static string SignedCall(Dictionary<string, string> sortedList)
    {
        var signature = sortedList
            .OrderBy(pair => pair.Key)
            .Select(pair => pair.Key + pair.Value)
            .Aggregate((first, second) => first + second);

        // LOCATION 2

        var utf8Encode = Encoding.UTF8.GetString(Encoding.Default.GetBytes(signature + _apiKeySig.ApiSig));

        // LOCATION 3

        var md5 = Md5.ComputeHash(utf8Encode);

        // LOCATION 4

        return md5;
    }

This is the MD5 code:

    public static string ComputeHash(string text)
    {
        var textBytes = Encoding.UTF8.GetBytes(text);
        var hash = _cryptoServiceProvider.ComputeHash(textBytes);
        return hash.Aggregate(string.Empty, (current, a) => current + a.ToString("x2"));
    }

Here are the contents of relevant variables at certain locations (included as comments in the code) when I run the code:

NOTE: I replaced the actual api_key and sk values with dummy values in all of the following results.

LOCATION 1: JSON-encoded queryParameters:

{
  "method": "track.scrobble",
  "api_key": "55555555555555555555555555555555",
  "sk": "66666666666666666666666666666666",
  "artist[0]": "Kiss",
  "track[0]": "2000 Man (Test Scrobble)",
  "album[0]": "Dynasty",
  "albumArtist[0]": "Kiss",
  "timestamp[0]": "1707606974",
  "artist[1]": "Kiss",
  "track[1]": "Charisma (Test Scrobble)",
  "album[1]": "Dynasty",
  "albumArtist[1]": "Kiss",
  "timestamp[1]": "1707606704"
}

LOCATION 2: Contents of signature:

album[0]Dynastyalbum[1]DynastyalbumArtist[0]KissalbumArtist[1]Kissapi_key55555555555555555555555555555555artist[0]Kissartist[1]Kissmethodtrack.scrobblesk66666666666666666666666666666666timestamp[0]1707606974timestamp[1]1707606704track[0]2000 Man (Test  Scrobble)track[1]Charisma (Test Scrobble)

LOCATION 3: Contents of utf8Encode:

album[0]Dynastyalbum[1]DynastyalbumArtist[0]KissalbumArtist[1]Kissapi_key55555555555555555555555555555555artist[0]Kissartist[1]Kissmethodtrack.scrobblesk66666666666666666666666666666666timestamp[0]1707606974timestamp[1]1707606704track[0]2000 Man (Test Scrobble)track[1]Charisma (Test Scrobble)77777777777777777777777777777777

LOCATION 4: Contents of md5:

113d8b41a4686abd54fb48452ab97027

LOCATION 5: Contents of queryString:

method=track.scrobble&api_key=55555555555555555555555555555555&sk=66666666666666666666666666666666&artist[0]=Kiss&track[0]=2000+Man+(Test+Scrobble)&album[0]=Dynasty&albumArtist[0]=Kiss&timestamp[0]=1707606974&artist[1]=Kiss&track[1]=Charisma+(Test+Scrobble)&album[1]=Dynasty&albumArtist[1]=Kiss&timestamp[1]=1707606704&api_sig=113d8b41a4686abd54fb48452ab97027

I obviously cannot see what is wrong. Could someone give me a clue or two.

0

There are 0 best solutions below