Very basic POC LastFM scrobbling working, and some changes to Discord RPC

This commit is contained in:
2026-04-16 20:42:42 +10:00
parent cceca25be9
commit 44cd9fd2e2
7 changed files with 484 additions and 2 deletions
+1 -1
View File
@@ -23,11 +23,11 @@ add_executable(ossp MACOSX_BUNDLE
socket/socketActions.c socket/socketActions.c
player/player.c player/player.c
player/playQueue.cpp player/playQueue.cpp
player/scrobbler_lastFm.c
libopensubsonic/crypto.c libopensubsonic/crypto.c
libopensubsonic/httpclient.c libopensubsonic/httpclient.c
libopensubsonic/logger.c libopensubsonic/logger.c
libopensubsonic/utils.c libopensubsonic/utils.c
libopensubsonic/scrobble_lastFm.c
libopensubsonic/scrobble_listenBrainz.c libopensubsonic/scrobble_listenBrainz.c
libopensubsonic/endpoint_getAlbum.c libopensubsonic/endpoint_getAlbum.c
libopensubsonic/endpoint_getAlbumList.c libopensubsonic/endpoint_getAlbumList.c
+4
View File
@@ -63,11 +63,13 @@ void discordrpc_update(discordrpc_data** discordrpc_struct) {
memset(&presence, 0, sizeof(presence)); memset(&presence, 0, sizeof(presence));
if ((*discordrpc_struct)->state == DISCORDRPC_STATE_IDLE) { if ((*discordrpc_struct)->state == DISCORDRPC_STATE_IDLE) {
printf("[DiscordRPC] Issuing Idle RPC.\n");
asprintf(&detailsString, "Idle"); asprintf(&detailsString, "Idle");
presence.details = detailsString; presence.details = detailsString;
} else if ((*discordrpc_struct)->state == DISCORDRPC_STATE_PLAYING_OPENSUBSONIC || } else if ((*discordrpc_struct)->state == DISCORDRPC_STATE_PLAYING_OPENSUBSONIC ||
((*discordrpc_struct)->state == DISCORDRPC_STATE_PLAYING_LOCALFILE)) { ((*discordrpc_struct)->state == DISCORDRPC_STATE_PLAYING_LOCALFILE)) {
// Playing a song from an OpenSubsonic server // Playing a song from an OpenSubsonic server
printf("[DiscordRPC] Issuing OpenSubsonic/Local File Song RPC.\n");
asprintf(&detailsString, "%s", (*discordrpc_struct)->songTitle); asprintf(&detailsString, "%s", (*discordrpc_struct)->songTitle);
asprintf(&stateString, "by %s", (*discordrpc_struct)->songArtist); asprintf(&stateString, "by %s", (*discordrpc_struct)->songArtist);
presence.details = detailsString; presence.details = detailsString;
@@ -83,6 +85,7 @@ void discordrpc_update(discordrpc_data** discordrpc_struct) {
} }
} else if ((*discordrpc_struct)->state == DISCORDRPC_STATE_PLAYING_INTERNETRADIO) { } else if ((*discordrpc_struct)->state == DISCORDRPC_STATE_PLAYING_INTERNETRADIO) {
// Playing an internet radio station // Playing an internet radio station
printf("[DiscordRPC] Issuing Internet Radio RPC.\n");
asprintf(&detailsString, "%s", (*discordrpc_struct)->songTitle); asprintf(&detailsString, "%s", (*discordrpc_struct)->songTitle);
asprintf(&stateString, "Internet radio station"); asprintf(&stateString, "Internet radio station");
presence.details = detailsString; presence.details = detailsString;
@@ -94,6 +97,7 @@ void discordrpc_update(discordrpc_data** discordrpc_struct) {
} }
} else if ((*discordrpc_struct)->state == DISCORDRPC_STATE_PAUSED) { } else if ((*discordrpc_struct)->state == DISCORDRPC_STATE_PAUSED) {
// Player is paused // Player is paused
printf("[DiscordRPC] Issuing Paused RPC.\n");
asprintf(&detailsString, "Paused"); asprintf(&detailsString, "Paused");
presence.details = detailsString; presence.details = detailsString;
} }
+6
View File
@@ -130,3 +130,9 @@ void OSSPQ_FreeSongObjectC(OSSPQ_SongStruct* songObjectC) {
if (songObjectC->coverArtUrl != NULL) { free(songObjectC->coverArtUrl); } if (songObjectC->coverArtUrl != NULL) { free(songObjectC->coverArtUrl); }
if (songObjectC != NULL) { free(songObjectC); } if (songObjectC != NULL) { free(songObjectC); }
} }
// Used for scrobbling, called every 200ms
long OSSPQ_getSongLength(int idx) {
OSSPQ_SongObject songObject = OSSPQ_SongQueue[idx];
return songObject.duration;
}
+1
View File
@@ -34,6 +34,7 @@ void OSSPQ_advancePos();
void OSSPQ_backtrackPos(); void OSSPQ_backtrackPos();
OSSPQ_SongStruct* OSSPQ_getAtPos(int pos); OSSPQ_SongStruct* OSSPQ_getAtPos(int pos);
void OSSPQ_FreeSongObjectC(OSSPQ_SongStruct* songObjectC); void OSSPQ_FreeSongObjectC(OSSPQ_SongStruct* songObjectC);
long OSSPQ_getSongLength(int idx);
#ifdef __cplusplus #ifdef __cplusplus
} }
+64 -1
View File
@@ -20,6 +20,9 @@
#include "playQueue.hpp" #include "playQueue.hpp"
#include "player.h" #include "player.h"
// TESTING
#include "scrobbler_lastFm.h"
extern configHandler_config_t* configObj; extern configHandler_config_t* configObj;
static int rc = 0; static int rc = 0;
GstElement *pipeline, *playbin, *filter_bin, *conv_in, *conv_out, *in_volume, *equalizer, *pitch, *reverb, *out_volume; GstElement *pipeline, *playbin, *filter_bin, *conv_in, *conv_out, *in_volume, *equalizer, *pitch, *reverb, *out_volume;
@@ -109,6 +112,7 @@ void* OSSPlayer_GMainLoop(void* arg) {
void* OSSPlayer_ThrdInit(void* arg) { void* OSSPlayer_ThrdInit(void* arg) {
(void)arg; (void)arg;
bool haveIssuedDiscordRPCIdle = true; bool haveIssuedDiscordRPCIdle = true;
bool haveScrobbledSong = false;
// Player init function for pthread entry // Player init function for pthread entry
logger_log_important(__func__, "Player thread running."); logger_log_important(__func__, "Player thread running.");
@@ -120,7 +124,9 @@ void* OSSPlayer_ThrdInit(void* arg) {
// Poll play queue for new items to play // Poll play queue for new items to play
while (true) { // TODO use global bool instead while (true) { // TODO use global bool instead
if (OSSPQ_getTotalPos() != 0 && isPlaying == false) { if (OSSPQ_getTotalPos() != 0 &&
OSSPQ_getCurrentPos() != OSSPQ_getTotalPos() &&
isPlaying == false) {
// Player is not playing and a song is in the song queue // Player is not playing and a song is in the song queue
// Pull new song from the song queue // Pull new song from the song queue
@@ -131,6 +137,9 @@ void* OSSPlayer_ThrdInit(void* arg) {
// TODO: this // TODO: this
} }
// Reset scrobble
haveScrobbledSong = false;
if (songObject->mode == OSSPQ_MODE_INTERNETRADIO) { if (songObject->mode == OSSPQ_MODE_INTERNETRADIO) {
// Setup Discord RPC // Setup Discord RPC
discordrpc_data* discordrpc = NULL; discordrpc_data* discordrpc = NULL;
@@ -147,6 +156,15 @@ void* OSSPlayer_ThrdInit(void* arg) {
isPlaying = true; isPlaying = true;
gst_element_set_state(pipeline, GST_STATE_PLAYING); gst_element_set_state(pipeline, GST_STATE_PLAYING);
} else if (songObject->mode == OSSPQ_MODE_OPENSUBSONIC) { } else if (songObject->mode == OSSPQ_MODE_OPENSUBSONIC) {
// Issue initial LastFM scrobble
scrobbler_data* scrobblerData = malloc(sizeof(scrobbler_data));
opensubsonic_scrobble_init(scrobblerData);
scrobblerData->songTitle = strdup(songObject->title);
scrobblerData->songAlbum = strdup(songObject->album);
scrobblerData->songArtist = strdup(songObject->artist);
opensubsonic_scrobble_lastFm(scrobblerData);
opensubsonic_scrobble_free(scrobblerData);
// Prepare Discord RPC // Prepare Discord RPC
discordrpc_data* discordrpc = NULL; discordrpc_data* discordrpc = NULL;
discordrpc_struct_init(&discordrpc); discordrpc_struct_init(&discordrpc);
@@ -210,6 +228,44 @@ void* OSSPlayer_ThrdInit(void* arg) {
} }
} }
// Scrobbler
// Nothing playing: 0.00
// Oh and end of song (EOS) -> 0.00
// If song is >3/4 finished, perform final scrobble
// Else, perform an in-progress scrobble every 45s (This can be handled later)
// Have to fetch the total playback from Gstreamer, otherwise im malloc'ing every 200ms, a little fucking dramatic
// NOTE: Cannot query Playbin3 for the length, as the OpenSubsonic /stream endpoint seems to be technically livestreaming it
// Bad idea: Could technically do it the same way the DiscordRPC one does it
// Gather the data at the same time, send a couple arguments and encapsulate it in it's own thread...
// Kinda wasteful of a process though
if (isPlaying == true) {
float songLength = (float)OSSPQ_getSongLength(OSSPQ_getCurrentPos());
printf("Song length: %f\n", songLength);
printf("Current: %f\n", OSSPlayer_GstECont_Playbin3_Position_Get());
// Check if song is >=3/4 finished
if (OSSPlayer_GstECont_Playbin3_Position_Get() >= (songLength / 4 * 3) && haveScrobbledSong == false) {
// Finalize song scrobble
OSSPQ_SongStruct* songObject = OSSPQ_getAtPos(OSSPQ_getCurrentPos());
scrobbler_data* scrobblerData = malloc(sizeof(scrobbler_data));
opensubsonic_scrobble_init(scrobblerData);
scrobblerData->finalize = 1;
scrobblerData->songTitle = strdup(songObject->title);
scrobblerData->songAlbum = strdup(songObject->album);
scrobblerData->songArtist = strdup(songObject->artist);
opensubsonic_scrobble_lastFm(scrobblerData);
opensubsonic_scrobble_free(scrobblerData);
haveScrobbledSong = true;
}
}
usleep(200 * 1000); usleep(200 * 1000);
} }
} }
@@ -563,3 +619,10 @@ void OSSPlayer_DiscordRPC_SendPlaying(time_t startTime) {
OSSPQ_FreeSongObjectC(songObject); OSSPQ_FreeSongObjectC(songObject);
} }
/*
* Functions that utilize scrobblers
*/
void OSSPlayer_Scrobbler_LastFM(int final) {
//
}
+384
View File
@@ -0,0 +1,384 @@
/*
* OpenSubSonicPlayer
* Goldenkrew3000 2026
* License: GNU General Public License 3.0
* LastFM Scrobbler
*/
#include <stdio.h>
#include <stdint.h>
#include <time.h>
#include "../libopensubsonic/httpclient.h"
#include "../external/cJSON.h"
#include "../external/md5.h"
#include "../external/libcurl_uriescape.h"
#include "../configHandler.h"
#include "scrobbler_lastFm.h"
// Temp - move away from that fuckahh logger
#include "../libopensubsonic/logger.h"
const char* lastFmScrobbleURL = "https://ws.audioscrobbler.com/2.0/";
static int rc = 0;
extern configHandler_config_t* configObj;
int opensubsonic_scrobble_lastFm(scrobbler_data* scrobblerData) {
if (scrobblerData->finalize) {
logger_log_general(__func__, "Performing final scrobble to LastFM.");
} else {
logger_log_general(__func__, "Performing in-progress scrobble to LastFM.");
}
// Fetch the current UNIX timestamp
time_t currentTime;
char* currentTime_string;
currentTime = time(NULL);
rc = asprintf(&currentTime_string, "%ld", currentTime);
if (rc == -1) {
logger_log_error(__func__, "asprintf() failed (Could not make char* of UNIX timestamp).");
return 1;
}
// Assemble the signature
char* sig_plaintext = NULL;
if (scrobblerData->finalize) {
rc = asprintf(&sig_plaintext, "album%salbumArtist%sapi_key%sartist%smethodtrack.scrobblesk%stimestamp%strack%s%s",
scrobblerData->songAlbum, scrobblerData->songArtist, configObj->lastfm_api_key, scrobblerData->songArtist,
configObj->lastfm_api_session_key, currentTime_string, scrobblerData->songTitle, configObj->lastfm_api_secret);
} else {
rc = asprintf(&sig_plaintext, "album%salbumArtist%sapi_key%sartist%smethodtrack.updateNowPlayingsk%stimestamp%strack%s%s",
scrobblerData->songAlbum, scrobblerData->songArtist, configObj->lastfm_api_key, scrobblerData->songArtist,
configObj->lastfm_api_session_key, currentTime_string, scrobblerData->songTitle, configObj->lastfm_api_secret);
}
if (rc == -1) {
logger_log_error(__func__, "asprintf() failed (Could not assemble plaintext signature).");
return 1;
}
uint8_t sig_md5_bytes[16];
char* sig_md5_text = NULL; // TODO do I have to free this? Also is be used in crypto.c
md5String(sig_plaintext, sig_md5_bytes);
free(sig_plaintext);
rc = asprintf(&sig_md5_text, "%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x",
sig_md5_bytes[0], sig_md5_bytes[1], sig_md5_bytes[2], sig_md5_bytes[3],
sig_md5_bytes[4], sig_md5_bytes[5], sig_md5_bytes[6], sig_md5_bytes[7],
sig_md5_bytes[8], sig_md5_bytes[9], sig_md5_bytes[10], sig_md5_bytes[11],
sig_md5_bytes[12], sig_md5_bytes[13], sig_md5_bytes[14], sig_md5_bytes[15]);
if (rc == -1) {
logger_log_error(__func__, "asprintf() failed (Could not assemble md5 signature).");
return 1;
}
// URI encode strings
char* uri_songTitle = lcue_uriescape(scrobblerData->songTitle, (unsigned int)strlen(scrobblerData->songTitle));
char* uri_songArtist = lcue_uriescape(scrobblerData->songArtist, (unsigned int)strlen(scrobblerData->songArtist));
char* uri_songAlbum = lcue_uriescape(scrobblerData->songAlbum, (unsigned int)strlen(scrobblerData->songAlbum));
if (uri_songTitle == NULL || uri_songArtist == NULL || uri_songAlbum == NULL) {
logger_log_error(__func__, "lcue_uriescape() error (Could not URI escape required strings).");
free(currentTime_string);
free(sig_md5_text);
if (uri_songTitle != NULL) { free(uri_songTitle); }
if (uri_songArtist != NULL) { free(uri_songArtist); }
if (uri_songAlbum != NULL) { free(uri_songAlbum); }
return 1;
}
// Assemble the payload
char* payload = NULL;
if (scrobblerData->finalize) {
rc = asprintf(&payload,
"%s?method=track.scrobble&api_key=%s&timestamp=%s&track=%s&artist=%s&album=%s&albumArtist=%s&sk=%s&api_sig=%s&format=json",
lastFmScrobbleURL, configObj->lastfm_api_key, currentTime_string, uri_songTitle,
uri_songArtist, uri_songAlbum, uri_songArtist,
configObj->lastfm_api_session_key, sig_md5_text);
} else {
rc = asprintf(&payload,
"%s?method=track.updateNowPlaying&api_key=%s&timestamp=%s&track=%s&artist=%s&album=%s&albumArtist=%s&sk=%s&api_sig=%s&format=json",
lastFmScrobbleURL, configObj->lastfm_api_key, currentTime_string, uri_songTitle,
uri_songArtist, uri_songAlbum, uri_songArtist,
configObj->lastfm_api_session_key, sig_md5_text);
}
free(currentTime_string);
free(sig_md5_text);
free(uri_songTitle);
free(uri_songAlbum);
free(uri_songArtist);
if (rc == -1) {
logger_log_error(__func__, "asprintf() failed (Could not assemble payload).");
return 1;
}
// Send scrobble and receive response
opensubsonic_httpClientRequest_t* httpReq;
opensubsonic_httpClient_prepareRequest(&httpReq);
httpReq->requestUrl = strdup(payload);
free(payload);
httpReq->scrobbler = SCROBBLER_LASTFM;
httpReq->method = HTTP_METHOD_POST;
opensubsonic_httpClient_request(&httpReq);
if (httpReq->responseCode != HTTP_CODE_SUCCESS) {
logger_log_error(__func__, "HTTP POST did not return success (%d).", httpReq->responseCode);
opensubsonic_httpClient_cleanup(&httpReq);
// TODO return error
}
// Parse the scrobble response
cJSON* root = cJSON_Parse(httpReq->responseMsg);
opensubsonic_httpClient_cleanup(&httpReq);
if (root == NULL) {
logger_log_error(__func__, "Error parsing JSON.");
// TODO return error
}
cJSON* inner_root = NULL;
cJSON* scrobbles_root = NULL; // Parent of inner_root, only used on final scrobble
if (scrobblerData->finalize) {
// Make an object from scrobbles
scrobbles_root = cJSON_GetObjectItemCaseSensitive(root, "scrobbles");
if (scrobbles_root == NULL) {
logger_log_error(__func__, "Error parsing JSON - scrobbles does not exist.");
cJSON_Delete(root);
// TODO return error
}
// Make an object from scrobble
inner_root = cJSON_GetObjectItemCaseSensitive(scrobbles_root, "scrobble");
if (inner_root == NULL) {
logger_log_error(__func__, "Error parsing JSON - scrobble does not exist.");
cJSON_Delete(root);
// TODO return error
}
} else {
// Make an object from nowplaying
inner_root = cJSON_GetObjectItemCaseSensitive(root, "nowplaying");
if (inner_root == NULL) {
logger_log_error(__func__, "Error parsing JSON - nowplaying does not exist.");
cJSON_Delete(root);
// TODO return error
}
}
// Make an object from artist, track, albumArtist, and album, and fetch codes
cJSON* artist_root = cJSON_GetObjectItemCaseSensitive(inner_root, "artist");
if (artist_root == NULL) {
logger_log_error(__func__, "Error parsing JSON - artist does not exist.");
cJSON_Delete(root);
// TODO return error
}
cJSON* artist_corrected = cJSON_GetObjectItemCaseSensitive(artist_root, "corrected");
if (cJSON_IsString(artist_corrected) && artist_corrected->valuestring != NULL) {
if (*(artist_corrected->valuestring) == '\0') {
logger_log_error(__func__, "Error parsing JSON - artist/corrected is empty.");
cJSON_Delete(root);
// TODO return error
}
char* endptr;
int corrected = (int)strtol(artist_corrected->valuestring, &endptr, 10);
if (*endptr != '\0') {
logger_log_error(__func__, "Error parsing JSON - artist/corrected strtol/endptr is not empty.");
cJSON_Delete(root);
// TODO return error
}
if (corrected == 1) {
logger_log_important(__func__, "Warning - Artist has been autocorrected.");
}
} else {
logger_log_error(__func__, "Error parsing JSON - artist/corrected does not exist.");
cJSON_Delete(root);
// TODO return error
}
cJSON* track_root = cJSON_GetObjectItemCaseSensitive(inner_root, "track");
if (track_root == NULL) {
logger_log_error(__func__, "Error parsing JSON - track does not exist.");
cJSON_Delete(root);
// TODO return error
}
cJSON* track_corrected = cJSON_GetObjectItemCaseSensitive(track_root, "corrected");
if (cJSON_IsString(track_corrected) && track_corrected->valuestring != NULL) {
if (*(track_corrected->valuestring) == '\0') {
logger_log_error(__func__, "Error parsing JSON - track/corrected is empty.");
cJSON_Delete(root);
// TODO return error
}
char* endptr;
int corrected = (int)strtol(track_corrected->valuestring, &endptr, 10);
if (*endptr != '\0') {
logger_log_error(__func__, "Error parsing JSON - track/corrected strtol/endptr is not empty.");
cJSON_Delete(root);
// TODO return error
}
if (corrected == 1) {
logger_log_important(__func__, "Warning - Track has been autocorrected.");
}
} else {
logger_log_error(__func__, "Error parsing JSON - track/corrected does not exist.");
cJSON_Delete(root);
// TODO return error
}
cJSON* albumArtist_root = cJSON_GetObjectItemCaseSensitive(inner_root, "albumArtist");
if (albumArtist_root == NULL) {
logger_log_error(__func__, "Error parsing JSON - albumArtist does not exist.");
cJSON_Delete(root);
// TODO return error
}
cJSON* albumArtist_corrected = cJSON_GetObjectItemCaseSensitive(albumArtist_root, "corrected");
if (cJSON_IsString(albumArtist_corrected) && albumArtist_corrected->valuestring != NULL) {
if (*(albumArtist_corrected->valuestring) == '\0') {
logger_log_error(__func__, "Error parsing JSON - albumArtist/corrected is empty.");
cJSON_Delete(root);
// TODO return error
}
char* endptr;
int corrected = (int)strtol(albumArtist_corrected->valuestring, &endptr, 10);
if (*endptr != '\0') {
logger_log_error(__func__, "Error parsing JSON - albumArtist/corrected strtol/endptr is not empty.");
cJSON_Delete(root);
// TODO return error
}
if (corrected == 1) {
logger_log_important(__func__, "Warning - Album Artist has been autocorrected.");
}
} else {
logger_log_error(__func__, "Error parsing JSON - albumArtist/corrected does not exist.");
cJSON_Delete(root);
// TODO return error
}
cJSON* album_root = cJSON_GetObjectItemCaseSensitive(inner_root, "album");
if (album_root == NULL) {
logger_log_error(__func__, "Error parsing JSON - album does not exist.");
cJSON_Delete(root);
// TODO return error
}
cJSON* album_corrected = cJSON_GetObjectItemCaseSensitive(album_root, "corrected");
if (cJSON_IsString(album_corrected) && album_corrected->valuestring != NULL) {
if (*(album_corrected->valuestring) == '\0') {
logger_log_error(__func__, "Error parsing JSON - album/corrected is empty.");
cJSON_Delete(root);
// TODO return error
}
char* endptr;
int corrected = (int)strtol(album_corrected->valuestring, &endptr, 10);
if (*endptr != '\0') {
logger_log_error(__func__, "Error parsing JSON - album/corrected strtol/endptr is not empty.");
cJSON_Delete(root);
// TODO return error
}
if (corrected == 1) {
logger_log_important(__func__, "Warning - Album has been autocorrected.");
}
} else {
logger_log_error(__func__, "Error parsing JSON - album/corrected does not exist.");
cJSON_Delete(root);
// TODO return error
}
// Make an object from ignoredMessage, and check return code
cJSON* ignoredMessage_root = cJSON_GetObjectItemCaseSensitive(inner_root, "ignoredMessage");
if (ignoredMessage_root == NULL) {
logger_log_error(__func__, "Error parsing JSON - ignoredMessage does not exist.");
cJSON_Delete(root);
// TODO return error
}
cJSON* ignoredMessage_code = cJSON_GetObjectItemCaseSensitive(ignoredMessage_root, "code");
if (cJSON_IsString(ignoredMessage_code) && ignoredMessage_code->valuestring != NULL) {
if (*(ignoredMessage_code->valuestring) == '\0') {
logger_log_error(__func__, "Error parsing JSON - ignoredMessage/code is empty.");
cJSON_Delete(root);
// TODO return error
}
char* endptr;
int code = (int)strtol(ignoredMessage_code->valuestring, &endptr, 10);
if (*endptr != '\0') {
logger_log_error(__func__, "Error parsing JSON - ignoredMessage/code strtol/endptr is not empty.");
cJSON_Delete(root);
// TODO return error
}
if (code == 0) {
if (!scrobblerData->finalize) {
logger_log_general(__func__, "In progress scrobble was successful.");
} else {
logger_log_general(__func__, "Final scrobble 1/2 was successful.");
}
} else if (code == 1) {
logger_log_error(__func__, "Artist was ignored.");
} else if (code == 2) {
logger_log_error(__func__, "Track was ignored.");
} else if (code == 3) {
logger_log_error(__func__, "Timestamp was too old.");
} else if (code == 4) {
logger_log_error(__func__, "Timestamp was too new.");
} else if (code == 5) {
logger_log_error(__func__, "Daily scrobble limit exceeded.");
} else {
logger_log_error(__func__, "Unknown error code received (%d)", code);
}
} else {
logger_log_error(__func__, "Error parsing JSON - ignoredMessage/code does not exist.");
cJSON_Delete(root);
// TODO return error
}
if (scrobblerData->finalize) {
cJSON* attr_root = cJSON_GetObjectItemCaseSensitive(scrobbles_root, "@attr");
if (attr_root == NULL) {
logger_log_error(__func__, "Error parsing JSON - @attr does not exist.");
cJSON_Delete(root);
// TODO return error
}
cJSON* attr_ignored = cJSON_GetObjectItemCaseSensitive(attr_root, "ignored");
if (cJSON_IsNumber(attr_ignored)) {
if (attr_ignored->valueint != 0) {
logger_log_important(__func__, "Warning - @attr/ignored is not 0 (%d).", attr_ignored->valueint);
}
} else {
logger_log_error(__func__, "Error parsing JSON - @attr/ignored does not exist.");
cJSON_Delete(root);
// TODO return error
}
cJSON* attr_accepted = cJSON_GetObjectItemCaseSensitive(attr_root, "accepted");
if (cJSON_IsNumber(attr_accepted)) {
if (attr_accepted->valueint != 1) {
logger_log_important(__func__, "Warning - @attr/accepted is not 1 (%d).", attr_accepted->valueint);
}
} else {
logger_log_error(__func__, "Error parsing JSON - @attr/accepted does not exist");
cJSON_Delete(root);
// TODO return error
}
// At this point, attr_ignored and attr_accepted are both known to be valid
if (attr_ignored->valueint == 0 && attr_accepted->valueint == 1) {
logger_log_general(__func__, "Final scrobble 2/2 was successful.");
} else {
logger_log_important(__func__, "Final scobble 2/2 was not successful (ignored: %d, accepted: %d",
attr_ignored->valueint, attr_accepted->valueint);
}
}
cJSON_Delete(root);
}
void opensubsonic_scrobble_init(scrobbler_data* scrobblerData) {
scrobblerData->finalize = 0;
scrobblerData->songTitle = NULL;
scrobblerData->songAlbum = NULL;
scrobblerData->songArtist = NULL;
}
void opensubsonic_scrobble_free(scrobbler_data* scrobblerData) {
if (scrobblerData->songTitle != NULL) { free(scrobblerData->songTitle); }
if (scrobblerData->songAlbum != NULL) { free(scrobblerData->songAlbum); }
if (scrobblerData->songArtist != NULL) { free(scrobblerData->songArtist); }
}
+24
View File
@@ -0,0 +1,24 @@
/*
* OpenSubSonicPlayer
* Goldenkrew3000 2026
* License: GNU General Public License 3.0
* LastFM Scrobbler
*/
#ifndef _SCROBBLER_LASTFM_H
#define _SCROBBLER_LASTFM_H
typedef struct {
int finalize; // 0 -> In progress, 1 -> Finalize
char* songTitle;
char* songAlbum;
char* songArtist;
} scrobbler_data;
int opensubsonic_scrobble_lastFm(scrobbler_data* scrobblerData);
void opensubsonic_scrobble_init(scrobbler_data* scrobblerData);
void opensubsonic_scrobble_free(scrobbler_data* scrobblerData);
// TODO fix this fuckass naming scheme
#endif