diff --git a/owntone.8 b/owntone.8 index f4203165..a3ff0211 100644 --- a/owntone.8 +++ b/owntone.8 @@ -20,7 +20,7 @@ Debug domains; available domains are: \fIconfig\fP, \fIdaap\fP, \fIdb\fP, \fIhttpd\fP, \fImain\fP, \fImdns\fP, \fImisc\fP, \fIrsp\fP, \fIscan\fP, \fIxcode\fP, \fIevent\fP, \fIhttp\fP, \fIremote\fP, \fIdacp\fP, \fIffmpeg\fP, \fIartwork\fP, \fIplayer\fP, \fIraop\fP, -\fIlaudio\fP, \fIdmap\fP, \fIfdbperf\fP, \fIspotify\fP, \fIlastfm\fP, +\fIlaudio\fP, \fIdmap\fP, \fIfdbperf\fP, \fIspotify\fP, \fIscrobble\fP, \fIcache\fP, \fImpd\fP, \fIstream\fP, \fIcast\fP, \fIfifo\fP, \fIlib\fP, \fIweb\fP, \fIairplay\fP. .TP diff --git a/src/Makefile.am b/src/Makefile.am index 0a277642..9dd91b70 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -126,6 +126,7 @@ owntone_SOURCES = main.c \ evthr.c evthr.h \ $(SPOTIFY_SRC) \ $(LASTFM_SRC) \ + listenbrainz.c listenbrainz.h \ $(MPD_SRC) \ listener.c listener.h \ commands.c commands.h \ diff --git a/src/db.h b/src/db.h index e024df68..38ae5d5d 100644 --- a/src/db.h +++ b/src/db.h @@ -72,6 +72,7 @@ enum query_type { #define DB_ADMIN_START_TIME "start_time" #define DB_ADMIN_LASTFM_SESSION_KEY "lastfm_sk" #define DB_ADMIN_SPOTIFY_REFRESH_TOKEN "spotify_refresh_token" +#define DB_ADMIN_LISTENBRAINZ_TOKEN "listenbrainz_token" /* Max value for media_file_info->rating (valid range is from 0 to 100) */ #define DB_FILES_RATING_MAX 100 diff --git a/src/http.h b/src/http.h index a68c5d97..6b9e05ec 100644 --- a/src/http.h +++ b/src/http.h @@ -21,7 +21,7 @@ struct http_client_ctx */ const char *url; struct keyval *output_headers; - char *output_body; + const char *output_body; /* A keyval/evbuf to store response headers and body. * Can be set to NULL to ignore that part of the response. @@ -37,10 +37,6 @@ struct http_client_ctx /* HTTP Response code */ int response_code; - - /* Private */ - int ret; - void *evbase; }; struct http_icy_metadata diff --git a/src/httpd.c b/src/httpd.c index b9c41343..2d8c04b6 100644 --- a/src/httpd.c +++ b/src/httpd.c @@ -54,6 +54,7 @@ #ifdef LASTFM # include "lastfm.h" #endif +#include "listenbrainz.h" #ifdef HAVE_LIBWEBSOCKETS # include "websocket.h" #endif @@ -162,16 +163,17 @@ playcount_inc_cb(void *arg) db_file_inc_playcount(*id); } -#ifdef LASTFM /* Callback from the worker thread (async operation as it may block) */ static void scrobble_cb(void *arg) { int *id = arg; +#ifdef LASTFM lastfm_scrobble(*id); -} #endif + listenbrainz_scrobble(*id); +} static const char * content_type_from_ext(const char *ext) @@ -672,9 +674,7 @@ stream_end_register(struct stream_ctx *st) { st->no_register_playback = true; worker_execute(playcount_inc_cb, &st->id, sizeof(int), 0); -#ifdef LASTFM worker_execute(scrobble_cb, &st->id, sizeof(int), 1); -#endif } } diff --git a/src/httpd_jsonapi.c b/src/httpd_jsonapi.c index f7e349c2..5e39474e 100644 --- a/src/httpd_jsonapi.c +++ b/src/httpd_jsonapi.c @@ -47,6 +47,7 @@ # include "lastfm.h" #endif #include "library.h" +#include "listenbrainz.h" #include "logger.h" #include "misc.h" #include "misc_json.h" @@ -1447,6 +1448,77 @@ jsonapi_reply_lastfm_logout(struct httpd_request *hreq) return HTTP_NOCONTENT; } +static int +jsonapi_reply_listenbrainz(struct httpd_request *hreq) +{ + struct listenbrainz_status status; + json_object *jreply; + + listenbrainz_status_get(&status); + + CHECK_NULL(L_WEB, jreply = json_object_new_object()); + + json_object_object_add(jreply, "enabled", json_object_new_boolean(!status.disabled)); + json_object_object_add(jreply, "token_valid", json_object_new_boolean(status.token_valid)); + if (status.user_name) + json_object_object_add(jreply, "user_name", json_object_new_string(status.user_name)); + if (status.message) + json_object_object_add(jreply, "message", json_object_new_string(status.message)); + + + CHECK_ERRNO(L_WEB, evbuffer_add_printf(hreq->out_body, "%s", json_object_to_json_string(jreply))); + + jparse_free(jreply); + listenbrainz_status_free(&status, true); + + return HTTP_OK; +} + +static int +jsonapi_reply_listenbrainz_token_add(struct httpd_request *hreq) +{ + json_object *request; + const char *token; + int ret; + + request = jparse_obj_from_evbuffer(hreq->in_body); + if (!request) + { + DPRINTF(E_LOG, L_WEB, "Failed to parse incoming request\n"); + return HTTP_BADREQUEST; + } + + token = jparse_str_from_obj(request, "token"); + + ret = listenbrainz_token_set(token); + + jparse_free(request); + + if (ret < 0) + { + DPRINTF(E_LOG, L_WEB, "Failed to set ListenBrainz token\n"); + return HTTP_INTERNAL; + } + + return HTTP_NOCONTENT; +} + +static int +jsonapi_reply_listenbrainz_token_delete(struct httpd_request *hreq) +{ + int ret; + + ret = listenbrainz_token_delete(); + + if (ret < 0) + { + DPRINTF(E_LOG, L_WEB, "Failed to delete ListenBrainz token\n"); + return HTTP_INTERNAL; + } + + return HTTP_NOCONTENT; +} + /* * Kicks off pairing of a daap/dacp client * @@ -4658,6 +4730,10 @@ static struct httpd_uri_map adm_handlers[] = { HTTPD_METHOD_GET, "^/api/search$", jsonapi_reply_search }, + { HTTPD_METHOD_GET, "^/api/listenbrainz$", jsonapi_reply_listenbrainz }, + { HTTPD_METHOD_POST, "^/api/listenbrainz/token$", jsonapi_reply_listenbrainz_token_add }, + { HTTPD_METHOD_DELETE, "^/api/listenbrainz/token$", jsonapi_reply_listenbrainz_token_delete }, + { 0, NULL, NULL } }; diff --git a/src/lastfm.c b/src/lastfm.c index 0a75c33f..9c040492 100644 --- a/src/lastfm.c +++ b/src/lastfm.c @@ -84,7 +84,7 @@ param_sign(struct keyval *kv) if (gc_err != GPG_ERR_NO_ERROR) { gpg_strerror_r(gc_err, ebuf, sizeof(ebuf)); - DPRINTF(E_LOG, L_LASTFM, "Could not open MD5: %s\n", ebuf); + DPRINTF(E_LOG, L_SCROBBLE, "lastfm: Could not open MD5: %s\n", ebuf); return -1; } @@ -99,7 +99,7 @@ param_sign(struct keyval *kv) hash_bytes = gcry_md_read(md_hdl, GCRY_MD_MD5); if (!hash_bytes) { - DPRINTF(E_LOG, L_LASTFM, "Could not read MD5 hash\n"); + DPRINTF(E_LOG, L_SCROBBLE, "lastfm: Could not read MD5 hash\n"); return -1; } @@ -163,22 +163,22 @@ response_process(struct http_client_ctx *ctx, char **errmsg) body = (char *)evbuffer_pullup(ctx->input_body, -1); if (!body || (strlen(body) == 0)) { - DPRINTF(E_LOG, L_LASTFM, "Empty response\n"); + DPRINTF(E_LOG, L_SCROBBLE, "lastfm: Empty response\n"); return -1; } tree = xml_from_string(body); if (!tree) { - DPRINTF(E_LOG, L_LASTFM, "Failed to parse LastFM response:\n%s\n", body); + DPRINTF(E_LOG, L_SCROBBLE, "lastfm: Failed to parse LastFM response:\n%s\n", body); return -1; } error = xml_get_val(tree, "lfm/error"); if (error) { - DPRINTF(E_LOG, L_LASTFM, "Request to LastFM failed: %s\n", error); - DPRINTF(E_DBG, L_LASTFM, "LastFM response:\n%s\n", body); + DPRINTF(E_LOG, L_SCROBBLE, "lastfm: Request to LastFM failed: %s\n", error); + DPRINTF(E_DBG, L_SCROBBLE, "lastfm: LastFM response:\n%s\n", body); if (errmsg) *errmsg = atrim(error); @@ -187,12 +187,12 @@ response_process(struct http_client_ctx *ctx, char **errmsg) return -1; } - DPRINTF(E_SPAM, L_LASTFM, "LastFM response:\n%s\n", body); + DPRINTF(E_SPAM, L_SCROBBLE, "lastfm: LastFM response:\n%s\n", body); // Was it a scrobble request? Then do nothing. TODO: Check for error messages if (xml_get_node(tree, "lfm/scrobbles/scrobble")) { - DPRINTF(E_DBG, L_LASTFM, "Scrobble callback\n"); + DPRINTF(E_DBG, L_SCROBBLE, "lastfm: Scrobble callback\n"); xml_free(tree); return 0; } @@ -201,12 +201,12 @@ response_process(struct http_client_ctx *ctx, char **errmsg) sk = atrim(xml_get_val(tree, "lfm/session/key")); if (!sk) { - DPRINTF(E_LOG, L_LASTFM, "Session key not found\n"); + DPRINTF(E_LOG, L_SCROBBLE, "lastfm: Session key not found\n"); xml_free(tree); return -1; } - DPRINTF(E_INFO, L_LASTFM, "Got session key from LastFM: %s\n", sk); + DPRINTF(E_INFO, L_SCROBBLE, "lastfm: Got session key from LastFM: %s\n", sk); db_admin_set(DB_ADMIN_LASTFM_SESSION_KEY, sk); free(lastfm_session_key); @@ -233,25 +233,27 @@ static int request_post(const char *url, struct keyval *kv, char **errmsg) { struct http_client_ctx ctx; + char *request_body; int ret; // API requires that we MD5 sign sorted param (without "format" param) ret = param_sign(kv); if (ret < 0) { - DPRINTF(E_LOG, L_LASTFM, "Aborting request, param_sign failed\n"); + DPRINTF(E_LOG, L_SCROBBLE, "lastfm: Aborting request, param_sign failed\n"); return -1; } memset(&ctx, 0, sizeof(struct http_client_ctx)); - ctx.output_body = http_form_urlencode(kv); - if (!ctx.output_body) + request_body = http_form_urlencode(kv); + if (!request_body) { - DPRINTF(E_LOG, L_LASTFM, "Aborting request, http_form_urlencode failed\n"); + DPRINTF(E_LOG, L_SCROBBLE, "lastfm: Aborting request, http_form_urlencode failed\n"); return -1; } + ctx.output_body = request_body; ctx.url = url; ctx.input_body = evbuffer_new(); @@ -262,7 +264,7 @@ request_post(const char *url, struct keyval *kv, char **errmsg) ret = response_process(&ctx, errmsg); out_free_ctx: - free(ctx.output_body); + free(request_body); evbuffer_free(ctx.input_body); return ret; @@ -281,7 +283,7 @@ scrobble(int id) mfi = db_file_fetch_byid(id); if (!mfi) { - DPRINTF(E_LOG, L_LASTFM, "Scrobble failed, track id %d is unknown\n", id); + DPRINTF(E_LOG, L_SCROBBLE, "lastfm: Scrobble failed, track id %d is unknown\n", id); return -1; } @@ -327,7 +329,7 @@ scrobble(int id) return -1; } - DPRINTF(E_INFO, L_LASTFM, "Scrobbling '%s' by '%s'\n", keyval_get(kv, "track"), keyval_get(kv, "artist")); + DPRINTF(E_INFO, L_SCROBBLE, "lastfm: Scrobbling '%s' by '%s'\n", keyval_get(kv, "track"), keyval_get(kv, "artist")); ret = request_post(api_url, kv, NULL); @@ -367,7 +369,7 @@ 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); + DPRINTF(E_LOG, L_SCROBBLE, "lastfm: LastFM credentials file OK, logging in with username %s\n", user); // Stop active scrobbling session stop_scrobbling(); @@ -418,12 +420,12 @@ lastfm_logout(void) int lastfm_scrobble(int id) { - DPRINTF(E_DBG, L_LASTFM, "Got LastFM scrobble request\n"); - // LastFM is disabled because we already tried looking for a session key, but failed if (lastfm_disabled) return -1; + DPRINTF(E_DBG, L_SCROBBLE, "lastfm: Got LastFM scrobble request\n"); + return scrobble(id); } @@ -443,7 +445,7 @@ lastfm_init(void) ret = db_admin_get(&lastfm_session_key, DB_ADMIN_LASTFM_SESSION_KEY); if (ret < 0) { - DPRINTF(E_DBG, L_LASTFM, "No valid LastFM session key\n"); + DPRINTF(E_DBG, L_SCROBBLE, "lastfm: No valid LastFM session key\n"); lastfm_disabled = true; } diff --git a/src/listenbrainz.c b/src/listenbrainz.c new file mode 100644 index 00000000..6e35a666 --- /dev/null +++ b/src/listenbrainz.c @@ -0,0 +1,327 @@ +/* + * Copyright (C) 2025 Christian Meffert + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include +#include +#include + +#include "conffile.h" +#include "db.h" +#include "http.h" +#include "listenbrainz.h" +#include "logger.h" +#include "misc_json.h" + +static const char *listenbrainz_submit_listens_url = "https://api.listenbrainz.org/1/submit-listens"; +static const char *listenbrainz_validate_token_url = "https://api.listenbrainz.org/1/validate-token"; +static bool listenbrainz_disabled = true; +static char *listenbrainz_token = NULL; +static time_t listenbrainz_rate_limited_until = 0; + +static int +submit_listens(struct media_file_info *mfi) +{ + struct http_client_ctx ctx = { 0 }; + struct keyval kv_out = { 0 }; + struct keyval kv_in = { 0 }; + char auth_token[1024]; + json_object *request_body; + json_object *listens; + json_object *listen; + json_object *track_metadata; + json_object *additional_info; + const char *x_rate_limit_reset_in; + int32_t rate_limit_seconds = -1; + int ret; + + ctx.url = listenbrainz_submit_listens_url; + + // Set request headers + ctx.output_headers = &kv_out; + snprintf(auth_token, sizeof(auth_token), "Token %s", listenbrainz_token); + keyval_add(ctx.output_headers, "Authorization", auth_token); + keyval_add(ctx.output_headers, "Content-Type", "application/json"); + + // Set request body + request_body = json_object_new_object(); + json_object_object_add(request_body, "listen_type", json_object_new_string("single")); + listens = json_object_new_array(); + json_object_object_add(request_body, "payload", listens); + listen = json_object_new_object(); + json_object_array_add(listens, listen); + json_object_object_add(listen, "listened_at", json_object_new_int64((int64_t)time(NULL))); + track_metadata = json_object_new_object(); + json_object_object_add(listen, "track_metadata", track_metadata); + json_object_object_add(track_metadata, "artist_name", json_object_new_string(mfi->artist)); + json_object_object_add(track_metadata, "release_name", json_object_new_string(mfi->album)); + json_object_object_add(track_metadata, "track_name", json_object_new_string(mfi->title)); + additional_info = json_object_new_object(); + json_object_object_add(track_metadata, "additional_info", additional_info); + json_object_object_add(additional_info, "media_player", json_object_new_string(PACKAGE_NAME)); + json_object_object_add(additional_info, "media_player_version", json_object_new_string(PACKAGE_VERSION)); + json_object_object_add(additional_info, "submission_client", json_object_new_string(PACKAGE_NAME)); + json_object_object_add(additional_info, "submission_client_version", json_object_new_string(PACKAGE_VERSION)); + json_object_object_add(additional_info, "duration_ms", json_object_new_int((int32_t)mfi->song_length)); + ctx.output_body = json_object_to_json_string(request_body); + + // Create input evbuffer for the response body and keyval for response headers + ctx.input_headers = &kv_in; + + // Send POST request for submit-listens endpoint + ret = http_client_request(&ctx, NULL); + + // Process response + if (ret < 0) + { + DPRINTF(E_LOG, L_SCROBBLE, "lbrainz: Failed to scrobble '%s' by '%s'\n", mfi->title, mfi->artist); + goto out; + } + + if (ctx.response_code == HTTP_OK) + { + DPRINTF(E_INFO, L_SCROBBLE, "lbrainz: Scrobbled '%s' by '%s'\n", mfi->title, mfi->artist); + listenbrainz_rate_limited_until = 0; + } + else if (ctx.response_code == 401) + { + DPRINTF(E_LOG, L_SCROBBLE, "lbrainz: Failed to scrobble '%s' by '%s', unauthorized, disable scrobbling\n", mfi->title, + mfi->artist); + listenbrainz_disabled = true; + } + else if (ctx.response_code == 429) + { + x_rate_limit_reset_in = keyval_get(ctx.input_headers, "X-RateLimit-Reset-In"); + ret = safe_atoi32(x_rate_limit_reset_in, &rate_limit_seconds); + if (ret == 0 && rate_limit_seconds > 0) + { + listenbrainz_rate_limited_until = time(NULL) + rate_limit_seconds; + } + DPRINTF(E_INFO, L_SCROBBLE, "lbrainz: Failed to scrobble '%s' by '%s', rate limited for %d seconds\n", mfi->title, + mfi->artist, rate_limit_seconds); + } + else + { + DPRINTF(E_LOG, L_SCROBBLE, "lbrainz: Failed to scrobble '%s' by '%s', response code: %d\n", mfi->title, mfi->artist, + ctx.response_code); + } + +out: + + // Clean up + jparse_free(request_body); + keyval_clear(ctx.output_headers); + keyval_clear(ctx.input_headers); + + return ret; +} + +static int +validate_token(struct listenbrainz_status *status) +{ + struct http_client_ctx ctx = { 0 }; + struct keyval kv_out = { 0 }; + char auth_token[1024]; + char *response_body; + json_object *json_response = NULL; + int ret = 0; + + if (!listenbrainz_token) + return -1; + + ctx.url = listenbrainz_validate_token_url; + + // Set request headers + ctx.output_headers = &kv_out; + snprintf(auth_token, sizeof(auth_token), "Token %s", listenbrainz_token); + keyval_add(ctx.output_headers, "Authorization", auth_token); + + // Create input evbuffer for the response body + ctx.input_body = evbuffer_new(); + + // Send GET request for validate-token endpoint + ret = http_client_request(&ctx, NULL); + + // Parse response + // 0-terminate for safety + evbuffer_add(ctx.input_body, "", 1); + + response_body = (char *)evbuffer_pullup(ctx.input_body, -1); + if (!response_body || (strlen(response_body) == 0)) + { + DPRINTF(E_LOG, L_SCROBBLE, "lbrainz: Request for '%s' failed, response was empty\n", ctx.url); + goto out; + } + + json_response = json_tokener_parse(response_body); + if (!json_response) + DPRINTF(E_LOG, L_SCROBBLE, "lbrainz: JSON parser returned an error for '%s'\n", ctx.url); + + status->user_name = safe_strdup(jparse_str_from_obj(json_response, "user_name")); + status->token_valid = jparse_bool_from_obj(json_response, "valid"); + status->message = safe_strdup(jparse_str_from_obj(json_response, "message")); + listenbrainz_disabled = !status->token_valid; + +out: + + // Clean up + if (ctx.input_body) + evbuffer_free(ctx.input_body); + keyval_clear(ctx.output_headers); + + return ret; +} + +/* Thread: worker */ +int +listenbrainz_scrobble(int mfi_id) +{ + struct media_file_info *mfi; + int ret; + + if (listenbrainz_disabled) + return -1; + + if (listenbrainz_rate_limited_until > 0 && time(NULL) < listenbrainz_rate_limited_until) + { + DPRINTF(E_INFO, L_SCROBBLE, "lbrainz: Rate limited, not scrobbling\n"); + return -2; + } + + mfi = db_file_fetch_byid(mfi_id); + if (!mfi) + { + DPRINTF(E_LOG, L_SCROBBLE, "lbrainz: Scrobble failed, track id %d is unknown\n", mfi_id); + return -1; + } + + // Don't scrobble songs which are shorter than 30 sec + if (mfi->song_length < 30000) + goto noscrobble; + + // Don't scrobble non-music and radio stations + if ((mfi->media_kind != MEDIA_KIND_MUSIC) || (mfi->data_kind == DATA_KIND_HTTP)) + goto noscrobble; + + // Don't scrobble songs with unknown artist + if (strcmp(mfi->artist, CFG_NAME_UNKNOWN_ARTIST) == 0) + goto noscrobble; + + ret = submit_listens(mfi); + return ret; + +noscrobble: + free_mfi(mfi, 0); + + return -1; +} + +int +listenbrainz_token_set(const char *token) +{ + int ret; + + if (!token) + { + DPRINTF(E_DBG, L_SCROBBLE, "lbrainz: Failed to update ListenBrainz token, no token provided\n"); + return -1; + } + + ret = db_admin_set(DB_ADMIN_LISTENBRAINZ_TOKEN, token); + if (ret < 0) + { + DPRINTF(E_DBG, L_SCROBBLE, "lbrainz: Failed to update ListenBrainz token, DB update failed\n"); + } + else + { + free(listenbrainz_token); + listenbrainz_token = NULL; + ret = db_admin_get(&listenbrainz_token, DB_ADMIN_LISTENBRAINZ_TOKEN); + if (ret == 0) + listenbrainz_disabled = false; + } + + return ret; +} + +int +listenbrainz_token_delete(void) +{ + int ret; + + ret = db_admin_delete(DB_ADMIN_LISTENBRAINZ_TOKEN); + if (ret < 0) + { + DPRINTF(E_DBG, L_SCROBBLE, "lbrainz: Failed to delete ListenBrainz token, DB delete query failed\n"); + } + else + { + free(listenbrainz_token); + listenbrainz_token = NULL; + listenbrainz_disabled = true; + } + + return ret; +} + +int +listenbrainz_status_get(struct listenbrainz_status *status) +{ + int ret = 0; + + memset(status, 0, sizeof(struct listenbrainz_status)); + + if (listenbrainz_disabled) + { + status->disabled = true; + } + else + { + ret = validate_token(status); + } + return ret; +} + +void +listenbrainz_status_free(struct listenbrainz_status *status, bool content_only) +{ + free(status->user_name); + free(status->message); + if (!content_only) + free(status); +} + +/* Thread: main */ +int +listenbrainz_init(void) +{ + int ret; + + ret = db_admin_get(&listenbrainz_token, DB_ADMIN_LISTENBRAINZ_TOKEN); + listenbrainz_disabled = (ret < 0); + + if (listenbrainz_disabled) + { + DPRINTF(E_DBG, L_SCROBBLE, "lbrainz: No valid ListenBrainz token\n"); + } + + return 0; +} diff --git a/src/listenbrainz.h b/src/listenbrainz.h new file mode 100644 index 00000000..692854bd --- /dev/null +++ b/src/listenbrainz.h @@ -0,0 +1,25 @@ + +#ifndef __LISTENBRAINZ_H__ +#define __LISTENBRAINZ_H__ + +struct listenbrainz_status { + bool disabled; + char *user_name; + bool token_valid; + char *message; +}; + +int +listenbrainz_scrobble(int mfi_id); +int +listenbrainz_token_set(const char *token); +int +listenbrainz_token_delete(void); +int +listenbrainz_status_get(struct listenbrainz_status *status); +void +listenbrainz_status_free(struct listenbrainz_status *status, bool content_only); +int +listenbrainz_init(void); + +#endif /* !__LISTENBRAINZ_H__ */ diff --git a/src/logger.c b/src/logger.c index f93cc688..0429fafe 100644 --- a/src/logger.c +++ b/src/logger.c @@ -58,7 +58,7 @@ static uint32_t logger_repeat_counter; static uint32_t logger_last_hash; static char *logfilename; static FILE *logfile; -static char *labels[] = { "config", "daap", "db", "httpd", "http", "main", "mdns", "misc", "rsp", "scan", "xcode", "event", "remote", "dacp", "ffmpeg", "artwork", "player", "raop", "laudio", "dmap", "dbperf", "spotify", "lastfm", "cache", "mpd", "stream", "cast", "fifo", "lib", "web", "airplay", "rcp" }; +static char *labels[] = { "config", "daap", "db", "httpd", "http", "main", "mdns", "misc", "rsp", "scan", "xcode", "event", "remote", "dacp", "ffmpeg", "artwork", "player", "raop", "laudio", "dmap", "dbperf", "spotify", "scrobble", "cache", "mpd", "stream", "cast", "fifo", "lib", "web", "airplay", "rcp" }; static char *severities[] = { "FATAL", "LOG", "WARN", "INFO", "DEBUG", "SPAM" }; diff --git a/src/logger.h b/src/logger.h index bbbb54f1..d7099b02 100644 --- a/src/logger.h +++ b/src/logger.h @@ -28,7 +28,7 @@ #define L_DMAP 19 #define L_DBPERF 20 #define L_SPOTIFY 21 -#define L_LASTFM 22 +#define L_SCROBBLE 22 #define L_CACHE 23 #define L_MPD 24 #define L_STREAMING 25 diff --git a/src/main.c b/src/main.c index 659b18f5..95301614 100644 --- a/src/main.c +++ b/src/main.c @@ -72,6 +72,7 @@ #ifdef LASTFM # include "lastfm.h" #endif +#include "listenbrainz.h" #define PIDFILE STATEDIR "/run/" PACKAGE ".pid" #define WEB_ROOT DATADIR "/htdocs" @@ -833,6 +834,7 @@ main(int argc, char **argv) #ifdef LASTFM lastfm_init(); #endif + listenbrainz_init(); /* Start Remote pairing service */ ret = remote_pairing_init(); diff --git a/src/player.c b/src/player.c index bcbbcb7c..cb989966 100644 --- a/src/player.c +++ b/src/player.c @@ -91,6 +91,7 @@ #ifdef LASTFM # include "lastfm.h" #endif +#include "listenbrainz.h" // The interval between each tick of the playback clock in ms. This means that // we read 10 ms frames from the input and pass to the output, so the clock @@ -378,16 +379,17 @@ skipcount_inc_cb(void *arg) db_file_inc_skipcount(*id); } -#ifdef LASTFM // Callback from the worker thread (async operation as it may block) static void scrobble_cb(void *arg) { int *id = arg; +#ifdef LASTFM lastfm_scrobble(*id); -} #endif + listenbrainz_scrobble(*id); +} // This is just to be able to log the caller in a simple way #define status_update(x, y) status_update_impl((x), (y), __func__) @@ -1072,9 +1074,7 @@ event_play_eof() if (id != DB_MEDIA_FILE_NON_PERSISTENT_ID) { worker_execute(playcount_inc_cb, &id, sizeof(int), 5); -#ifdef LASTFM worker_execute(scrobble_cb, &id, sizeof(int), 8); -#endif history_add(pb_session.playing_now->id, pb_session.playing_now->item_id); }