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×tamp[0]=1707606974&artist[1]=Kiss&track[1]=Charisma+(Test+Scrobble)&album[1]=Dynasty&albumArtist[1]=Kiss×tamp[1]=1707606704&api_sig=113d8b41a4686abd54fb48452ab97027
I obviously cannot see what is wrong. Could someone give me a clue or two.