Very basic POC LastFM scrobbling working, and some changes to Discord RPC
This commit is contained in:
+1
-1
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
@@ -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) {
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|||||||
@@ -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(¤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); }
|
||||||
|
}
|
||||||
@@ -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
|
||||||
Reference in New Issue
Block a user