From 44cd9fd2e26aeadb92bf15b447350a5a15e646e0 Mon Sep 17 00:00:00 2001 From: Goldenkrew3000 Date: Thu, 16 Apr 2026 20:42:42 +1000 Subject: [PATCH] Very basic POC LastFM scrobbling working, and some changes to Discord RPC --- src/CMakeLists.txt | 2 +- src/discordrpc.c | 4 + src/player/playQueue.cpp | 6 + src/player/playQueue.hpp | 1 + src/player/player.c | 65 +++++- src/player/scrobbler_lastFm.c | 384 ++++++++++++++++++++++++++++++++++ src/player/scrobbler_lastFm.h | 24 +++ 7 files changed, 484 insertions(+), 2 deletions(-) create mode 100644 src/player/scrobbler_lastFm.c create mode 100644 src/player/scrobbler_lastFm.h diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 4d6b4cc..7da6a40 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -23,11 +23,11 @@ add_executable(ossp MACOSX_BUNDLE socket/socketActions.c player/player.c player/playQueue.cpp + player/scrobbler_lastFm.c libopensubsonic/crypto.c libopensubsonic/httpclient.c libopensubsonic/logger.c libopensubsonic/utils.c - libopensubsonic/scrobble_lastFm.c libopensubsonic/scrobble_listenBrainz.c libopensubsonic/endpoint_getAlbum.c libopensubsonic/endpoint_getAlbumList.c diff --git a/src/discordrpc.c b/src/discordrpc.c index 17c4cf9..7e92609 100644 --- a/src/discordrpc.c +++ b/src/discordrpc.c @@ -63,11 +63,13 @@ void discordrpc_update(discordrpc_data** discordrpc_struct) { memset(&presence, 0, sizeof(presence)); if ((*discordrpc_struct)->state == DISCORDRPC_STATE_IDLE) { + printf("[DiscordRPC] Issuing Idle RPC.\n"); asprintf(&detailsString, "Idle"); presence.details = detailsString; } else if ((*discordrpc_struct)->state == DISCORDRPC_STATE_PLAYING_OPENSUBSONIC || ((*discordrpc_struct)->state == DISCORDRPC_STATE_PLAYING_LOCALFILE)) { // Playing a song from an OpenSubsonic server + printf("[DiscordRPC] Issuing OpenSubsonic/Local File Song RPC.\n"); asprintf(&detailsString, "%s", (*discordrpc_struct)->songTitle); asprintf(&stateString, "by %s", (*discordrpc_struct)->songArtist); presence.details = detailsString; @@ -83,6 +85,7 @@ void discordrpc_update(discordrpc_data** discordrpc_struct) { } } else if ((*discordrpc_struct)->state == DISCORDRPC_STATE_PLAYING_INTERNETRADIO) { // Playing an internet radio station + printf("[DiscordRPC] Issuing Internet Radio RPC.\n"); asprintf(&detailsString, "%s", (*discordrpc_struct)->songTitle); asprintf(&stateString, "Internet radio station"); presence.details = detailsString; @@ -94,6 +97,7 @@ void discordrpc_update(discordrpc_data** discordrpc_struct) { } } else if ((*discordrpc_struct)->state == DISCORDRPC_STATE_PAUSED) { // Player is paused + printf("[DiscordRPC] Issuing Paused RPC.\n"); asprintf(&detailsString, "Paused"); presence.details = detailsString; } diff --git a/src/player/playQueue.cpp b/src/player/playQueue.cpp index 394e2a7..52efb4e 100644 --- a/src/player/playQueue.cpp +++ b/src/player/playQueue.cpp @@ -130,3 +130,9 @@ void OSSPQ_FreeSongObjectC(OSSPQ_SongStruct* songObjectC) { if (songObjectC->coverArtUrl != NULL) { free(songObjectC->coverArtUrl); } 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; +} diff --git a/src/player/playQueue.hpp b/src/player/playQueue.hpp index b10b8eb..bcc8809 100644 --- a/src/player/playQueue.hpp +++ b/src/player/playQueue.hpp @@ -34,6 +34,7 @@ void OSSPQ_advancePos(); void OSSPQ_backtrackPos(); OSSPQ_SongStruct* OSSPQ_getAtPos(int pos); void OSSPQ_FreeSongObjectC(OSSPQ_SongStruct* songObjectC); +long OSSPQ_getSongLength(int idx); #ifdef __cplusplus } diff --git a/src/player/player.c b/src/player/player.c index 2241ada..6fb18be 100644 --- a/src/player/player.c +++ b/src/player/player.c @@ -20,6 +20,9 @@ #include "playQueue.hpp" #include "player.h" +// TESTING +#include "scrobbler_lastFm.h" + extern configHandler_config_t* configObj; static int rc = 0; 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)arg; bool haveIssuedDiscordRPCIdle = true; + bool haveScrobbledSong = false; // Player init function for pthread entry logger_log_important(__func__, "Player thread running."); @@ -120,7 +124,9 @@ void* OSSPlayer_ThrdInit(void* arg) { // Poll play queue for new items to play 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 // Pull new song from the song queue @@ -131,6 +137,9 @@ void* OSSPlayer_ThrdInit(void* arg) { // TODO: this } + // Reset scrobble + haveScrobbledSong = false; + if (songObject->mode == OSSPQ_MODE_INTERNETRADIO) { // Setup Discord RPC discordrpc_data* discordrpc = NULL; @@ -147,6 +156,15 @@ void* OSSPlayer_ThrdInit(void* arg) { isPlaying = true; gst_element_set_state(pipeline, GST_STATE_PLAYING); } 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 discordrpc_data* discordrpc = NULL; 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); } } @@ -563,3 +619,10 @@ void OSSPlayer_DiscordRPC_SendPlaying(time_t startTime) { OSSPQ_FreeSongObjectC(songObject); } + +/* + * Functions that utilize scrobblers + */ +void OSSPlayer_Scrobbler_LastFM(int final) { + // +} diff --git a/src/player/scrobbler_lastFm.c b/src/player/scrobbler_lastFm.c new file mode 100644 index 0000000..4b614c2 --- /dev/null +++ b/src/player/scrobbler_lastFm.c @@ -0,0 +1,384 @@ +/* + * OpenSubSonicPlayer + * Goldenkrew3000 2026 + * License: GNU General Public License 3.0 + * LastFM Scrobbler + */ + +#include +#include +#include +#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(¤tTime_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×tamp=%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×tamp=%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); } +} diff --git a/src/player/scrobbler_lastFm.h b/src/player/scrobbler_lastFm.h new file mode 100644 index 0000000..0e2d2a6 --- /dev/null +++ b/src/player/scrobbler_lastFm.h @@ -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