diff --git a/src/httpd_jsonapi.c b/src/httpd_jsonapi.c index 044ecf2d..37001fee 100644 --- a/src/httpd_jsonapi.c +++ b/src/httpd_jsonapi.c @@ -41,6 +41,9 @@ #include "conffile.h" #include "db.h" #include "httpd.h" +#ifdef LASTFM +# include "lastfm.h" +#endif #include "library.h" #include "logger.h" #include "misc.h" @@ -416,6 +419,120 @@ jsonapi_reply_pairing(struct evhttp_request *req, struct evbuffer *evbuf, char * return pairing_get(evbuf); } +static int +jsonapi_reply_lastfm(struct evhttp_request *req, struct evbuffer *evbuf, char *uri, struct evkeyvalq *query) +{ + json_object *reply; + bool enabled = false; + bool scrobbling_enabled = false; + int ret; + +#ifdef LASTFM + enabled = true; + scrobbling_enabled = lastfm_is_enabled(); +#endif + + reply = json_object_new_object(); + json_object_object_add(reply, "enabled", json_object_new_boolean(enabled)); + json_object_object_add(reply, "scrobbling_enabled", json_object_new_boolean(scrobbling_enabled)); + + ret = evbuffer_add_printf(evbuf, "%s", json_object_to_json_string(reply)); + jparse_free(reply); + if (ret < 0) + { + DPRINTF(E_LOG, L_WEB, "LastFM: Couldn't add LastFM enabled to response buffer.\n"); + return -1; + } + + return 0; +} + +/* + * Endpoint to log into LastFM + */ +static int +jsonapi_reply_lastfm_login(struct evhttp_request *req, struct evbuffer *evbuf, char *uri, struct evkeyvalq *query) +{ +#ifdef LASTFM + struct evbuffer *in_evbuf; + json_object* request; + const char *user; + const char *password; + char *errmsg = NULL; + json_object* reply; + json_object* errors; + int ret; + + DPRINTF(E_DBG, L_WEB, "Received LastFM login request\n"); + + in_evbuf = evhttp_request_get_input_buffer(req); + request = jparse_obj_from_evbuffer(in_evbuf); + if (!request) + { + DPRINTF(E_LOG, L_WEB, "Failed to parse incoming request\n"); + return -1; + } + + reply = json_object_new_object(); + + user = jparse_str_from_obj(request, "user"); + password = jparse_str_from_obj(request, "password"); + if (user && strlen(user) > 0 && password && strlen(password) > 0) + { + ret = lastfm_login_user(user, password, &errmsg); + if (ret < 0) + { + json_object_object_add(reply, "success", json_object_new_boolean(false)); + errors = json_object_new_object(); + if (errmsg) + json_object_object_add(errors, "error", json_object_new_string(errmsg)); + else + json_object_object_add(errors, "error", json_object_new_string("Unknown error")); + json_object_object_add(reply, "errors", errors); + } + else + { + json_object_object_add(reply, "success", json_object_new_boolean(true)); + } + free(errmsg); + } + else + { + DPRINTF(E_LOG, L_WEB, "No user or password in LastFM login post request\n"); + + json_object_object_add(reply, "success", json_object_new_boolean(false)); + errors = json_object_new_object(); + if (!user || strlen(user) == 0) + json_object_object_add(errors, "user", json_object_new_string("Username is required")); + if (!password || strlen(password) == 0) + json_object_object_add(errors, "password", json_object_new_string("Password is required")); + json_object_object_add(reply, "errors", errors); + } + + ret = evbuffer_add_printf(evbuf, "%s", json_object_to_json_string(reply)); + jparse_free(reply); + if (ret < 0) + { + DPRINTF(E_LOG, L_WEB, "LastFM: Couldn't add LastFM login data to response buffer.\n"); + return -1; + } + +#else + DPRINTF(E_LOG, L_WEB, "Received LastFM login request but was not compiled with enable-lastfm\n"); +#endif + + return 0; +} + +static int +jsonapi_reply_lastfm_logout(struct evhttp_request *req, struct evbuffer *evbuf, char *uri, struct evkeyvalq *query) +{ +#ifdef LASTFM + lastfm_logout(); +#endif + return 0; +} + static struct uri_map adm_handlers[] = { { .regexp = "^/api/config", .handler = jsonapi_reply_config }, @@ -424,6 +541,9 @@ static struct uri_map adm_handlers[] = { .regexp = "^/api/spotify-login", .handler = jsonapi_reply_spotify_login }, { .regexp = "^/api/spotify", .handler = jsonapi_reply_spotify }, { .regexp = "^/api/pairing", .handler = jsonapi_reply_pairing }, + { .regexp = "^/api/lastfm-login", .handler = jsonapi_reply_lastfm_login }, + { .regexp = "^/api/lastfm-logout", .handler = jsonapi_reply_lastfm_logout }, + { .regexp = "^/api/lastfm", .handler = jsonapi_reply_lastfm }, { .regexp = NULL, .handler = NULL } }; diff --git a/src/lastfm.c b/src/lastfm.c index b6838516..43e4e7a3 100644 --- a/src/lastfm.c +++ b/src/lastfm.c @@ -28,6 +28,7 @@ #include #include #include +#include #include #include @@ -38,13 +39,14 @@ #include "db.h" #include "lastfm.h" +#include "listener.h" #include "logger.h" #include "misc.h" #include "http.h" // LastFM becomes disabled if we get a scrobble, try initialising session, // but can't (probably no session key in db because user does not use LastFM) -static int lastfm_disabled = 0; +static bool lastfm_disabled = false; /** * The API key and secret (not so secret being open source) is specific to @@ -117,15 +119,15 @@ param_sign(struct keyval *kv) /* --------------------------------- MAIN --------------------------------- */ -static void -response_proces(struct http_client_ctx *ctx) +static int +response_process(struct http_client_ctx *ctx, char **errmsg) { mxml_node_t *tree; mxml_node_t *s_node; mxml_node_t *e_node; char *body; - char *errmsg; char *sk; + int ret; // NULL-terminate the buffer evbuffer_add(ctx->input_body, "", 1); @@ -134,35 +136,39 @@ response_proces(struct http_client_ctx *ctx) if (!body || (strlen(body) == 0)) { DPRINTF(E_LOG, L_LASTFM, "Empty response\n"); - return; + return -1; } - DPRINTF(E_SPAM, L_LASTFM, "LastFM response:\n%s\n", body); - tree = mxmlLoadString(NULL, body, MXML_OPAQUE_CALLBACK); if (!tree) - return; + { + DPRINTF(E_LOG, L_LASTFM, "Failed to parse LastFM response:\n%s\n", body); + return -1; + } // Look for errors e_node = mxmlFindElement(tree, tree, "error", NULL, NULL, MXML_DESCEND); if (e_node) { - errmsg = trimwhitespace(mxmlGetOpaque(e_node)); - DPRINTF(E_LOG, L_LASTFM, "Request to LastFM failed: %s\n", errmsg); + DPRINTF(E_LOG, L_LASTFM, "Request to LastFM failed: %s\n", mxmlGetOpaque(e_node)); + DPRINTF(E_DBG, L_LASTFM, "LastFM response:\n%s\n", body); if (errmsg) - free(errmsg); + *errmsg = trimwhitespace(mxmlGetOpaque(e_node)); + mxmlDelete(tree); - return; + return -1; } + DPRINTF(E_SPAM, L_LASTFM, "LastFM response:\n%s\n", body); + // Was it a scrobble request? Then do nothing. TODO: Check for error messages s_node = mxmlFindElement(tree, tree, "scrobbles", NULL, NULL, MXML_DESCEND); if (s_node) { DPRINTF(E_DBG, L_LASTFM, "Scrobble callback\n"); mxmlDelete(tree); - return; + return 0; } // Otherwise an auth request, so get the session key @@ -171,7 +177,7 @@ response_proces(struct http_client_ctx *ctx) { DPRINTF(E_LOG, L_LASTFM, "Session key not found\n"); mxmlDelete(tree); - return; + return -1; } sk = trimwhitespace(mxmlGetOpaque(s_node)); @@ -184,28 +190,36 @@ response_proces(struct http_client_ctx *ctx) free(lastfm_session_key); lastfm_session_key = sk; + lastfm_disabled = false; + ret = 0; + } + else + { + ret = -1; } mxmlDelete(tree); + return ret; } +/* + * Post request against the Last.fm api + * + * Important: + * The Last.fm API requires that we MD5 sign sorted parameters (without "format" parameters), + * therefor the keyval parameters must be sorted alphabetically by their key. + * + * @param url API endpoint url + * @param kv Alphabetically sorted post parameters + * @param errmsg (Optional) returns the error message (or NULL) if request failed + */ static int -request_post(char *method, struct keyval *kv, int auth) +request_post(const char *url, struct keyval *kv, char **errmsg) { struct http_client_ctx ctx; int ret; - ret = keyval_add(kv, "method", method); - if (ret < 0) - return -1; - - if (!auth) - ret = keyval_add(kv, "sk", lastfm_session_key); - if (ret < 0) - return -1; - // API requires that we MD5 sign sorted param (without "format" param) - keyval_sort(kv); ret = param_sign(kv); if (ret < 0) { @@ -222,14 +236,14 @@ request_post(char *method, struct keyval *kv, int auth) return -1; } - ctx.url = auth ? auth_url : api_url; + ctx.url = url; ctx.input_body = evbuffer_new(); ret = http_client_request(&ctx); if (ret < 0) goto out_free_ctx; - response_proces(&ctx); + ret = response_process(&ctx, errmsg); out_free_ctx: free(ctx.output_body); @@ -275,16 +289,18 @@ scrobble(int id) snprintf(trackNumber, sizeof(trackNumber), "%" PRIu32, mfi->track); snprintf(timestamp, sizeof(timestamp), "%" PRIi64, (int64_t)time(NULL)); - ret = ( (keyval_add(kv, "api_key", lastfm_api_key) == 0) && - (keyval_add(kv, "sk", lastfm_session_key) == 0) && - (keyval_add(kv, "artist", mfi->artist) == 0) && - (keyval_add(kv, "track", mfi->title) == 0) && - (keyval_add(kv, "album", mfi->album) == 0) && - (keyval_add(kv, "albumArtist", mfi->album_artist) == 0) && - (keyval_add(kv, "duration", duration) == 0) && - (keyval_add(kv, "trackNumber", trackNumber) == 0) && - (keyval_add(kv, "timestamp", timestamp) == 0) - ); + ret = ( + (keyval_add(kv, "album", mfi->album) == 0) && + (keyval_add(kv, "albumArtist", mfi->album_artist) == 0) && + (keyval_add(kv, "api_key", lastfm_api_key) == 0) && + (keyval_add(kv, "artist", mfi->artist) == 0) && + (keyval_add(kv, "duration", duration) == 0) && + (keyval_add(kv, "method", "track.scrobble") == 0) && + (keyval_add(kv, "sk", lastfm_session_key) == 0) && + (keyval_add(kv, "timestamp", timestamp) == 0) && + (keyval_add(kv, "track", mfi->title) == 0) && + (keyval_add(kv, "trackNumber", trackNumber) == 0) + ); free_mfi(mfi, 0); @@ -297,7 +313,7 @@ scrobble(int id) DPRINTF(E_INFO, L_LASTFM, "Scrobbling '%s' by '%s'\n", keyval_get(kv, "track"), keyval_get(kv, "artist")); - ret = request_post("track.scrobble", kv, 0); + ret = request_post(api_url, kv, NULL); keyval_clear(kv); free(kv); @@ -314,40 +330,72 @@ scrobble(int id) /* ---------------------------- Our lastfm API --------------------------- */ -/* Thread: filescanner */ -void -lastfm_login(char **arglist) +/* Thread: filescanner, httpd */ +static void +stop_scrobbling() { - struct keyval *kv; - int ret; - - DPRINTF(E_LOG, L_LASTFM, "LastFM credentials file OK, logging in with username %s\n", arglist[0]); - // Delete any existing session key free(lastfm_session_key); lastfm_session_key = NULL; - db_admin_delete("lastfm_sk"); + // Disable LastFM, will be enabled after successful login request + lastfm_disabled = true; - // Enable LastFM now that we got a login attempt - lastfm_disabled = 0; + db_admin_delete("lastfm_sk"); +} + +/* Thread: filescanner, httpd */ +int +lastfm_login_user(const char *user, const char *password, char **errmsg) +{ + struct keyval *kv; + int ret; + + DPRINTF(E_LOG, L_LASTFM, "LastFM credentials file OK, logging in with username %s\n", user); + + // Stop active scrobbling session + stop_scrobbling(); kv = keyval_alloc(); if (!kv) - return; + return -1; - ret = ( (keyval_add(kv, "api_key", lastfm_api_key) == 0) && - (keyval_add(kv, "username", arglist[0]) == 0) && - (keyval_add(kv, "password", arglist[1]) == 0) ); + ret = ( + (keyval_add(kv, "api_key", lastfm_api_key) == 0) && + (keyval_add(kv, "method", "auth.getMobileSession") == 0) && + (keyval_add(kv, "password", password) == 0) && + (keyval_add(kv, "username", user) == 0) + ); if (!ret) goto out_free_kv; // Send the login request - request_post("auth.getMobileSession", kv, 1); + ret = request_post(auth_url, kv, errmsg); out_free_kv: keyval_clear(kv); free(kv); + + listener_notify(LISTENER_LASTFM); + + return ret; +} + +/* Thread: filescanner */ +void +lastfm_login(char **arglist) +{ + if (arglist) + lastfm_login_user(arglist[0], arglist[1], NULL); + else + lastfm_login_user(NULL, NULL, NULL); +} + +void +lastfm_logout(void) +{ + stop_scrobbling(); + listener_notify(LISTENER_LASTFM); } /* Thread: worker */ @@ -360,17 +408,27 @@ lastfm_scrobble(int id) if (lastfm_disabled) return -1; - // No session key in mem or in db - if (!lastfm_session_key) - lastfm_session_key = db_admin_get("lastfm_sk"); - - if (!lastfm_session_key) - { - DPRINTF(E_INFO, L_LASTFM, "No valid LastFM session key\n"); - lastfm_disabled = 1; - return -1; - } - return scrobble(id); } +/* Thread: httpd */ +bool +lastfm_is_enabled(void) +{ + return !lastfm_disabled; +} + +/* Thread: main */ +int +lastfm_init(void) +{ + lastfm_session_key = db_admin_get("lastfm_sk"); + if (!lastfm_session_key) + { + DPRINTF(E_DBG, L_LASTFM, "No valid LastFM session key\n"); + lastfm_disabled = true; + } + + return 0; +} + diff --git a/src/lastfm.h b/src/lastfm.h index 7c6d9fe2..f9951bc7 100644 --- a/src/lastfm.h +++ b/src/lastfm.h @@ -2,10 +2,24 @@ #ifndef __LASTFM_H__ #define __LASTFM_H__ +#include + +int +lastfm_login_user(const char *user, const char *password, char **errmsg); + void lastfm_login(char **arglist); +void +lastfm_logout(void); + int lastfm_scrobble(int id); +bool +lastfm_is_enabled(void); + +int +lastfm_init(void); + #endif /* !__LASTFM_H__ */ diff --git a/src/listener.h b/src/listener.h index c15cf712..f1aa65ca 100644 --- a/src/listener.h +++ b/src/listener.h @@ -24,6 +24,8 @@ enum listener_event_type LISTENER_PAIRING = (1 << 8), /* Spotify status changes (login, logout) */ LISTENER_SPOTIFY = (1 << 9), + /* Last.fm status changes (enable/disable scrobbling) */ + LISTENER_LASTFM = (1 << 10), }; typedef void (*notify)(enum listener_event_type type); diff --git a/src/main.c b/src/main.c index bc8b6006..c0db5163 100644 --- a/src/main.c +++ b/src/main.c @@ -70,6 +70,9 @@ GCRY_THREAD_OPTION_PTHREAD_IMPL; #include "player.h" #include "worker.h" #include "library.h" +#ifdef LASTFM +# include "lastfm.h" +#endif #ifdef HAVE_LIBCURL # include @@ -784,6 +787,10 @@ main(int argc, char **argv) mdns_no_mpd = 1; #endif +#ifdef LASTFM + lastfm_init(); +#endif + /* Start Remote pairing service */ ret = remote_pairing_init(); if (ret != 0) diff --git a/src/websocket.c b/src/websocket.c index eec7bc04..bf323db0 100644 --- a/src/websocket.c +++ b/src/websocket.c @@ -137,6 +137,10 @@ process_notify_request(struct ws_session_data_notify *session_data, void *in, si { session_data->events |= LISTENER_SPOTIFY; } + else if (0 == strcmp(event_type, "lastfm")) + { + session_data->events |= LISTENER_LASTFM; + } } } } @@ -177,6 +181,10 @@ send_notify_reply(short events, struct lws* wsi) { json_object_array_add(notify, json_object_new_string("spotify")); } + if (events & LISTENER_LASTFM) + { + json_object_array_add(notify, json_object_new_string("lastfm")); + } reply = json_object_new_object(); json_object_object_add(reply, "notify", notify); @@ -258,7 +266,7 @@ static struct lws_protocols protocols[] = static void * websocket(void *arg) { - listener_add(listener_cb, LISTENER_UPDATE | LISTENER_PAIRING | LISTENER_SPOTIFY); + listener_add(listener_cb, LISTENER_UPDATE | LISTENER_PAIRING | LISTENER_SPOTIFY | LISTENER_LASTFM); while(!ws_exit) {