[vlc-devel] [PATCH] Add module to submit listens to ListenBrainz

Kartik Ohri kartikohri13@gmail.com kartikohri13 at gmail.com
Tue Mar 3 20:05:27 CET 2020


VLC already has the audioscrobbler module to submit scrobbles to
last.fm and other services with a similar API. This module extends
that functionality to allow submission of listens to ListenBrainz.
The existing audioscrobbler module is incompatible with ListenBrainz
due to difference in authentication procedures and REST API for
submissions.

The term scrobble is a trademarked term by Last.fm, therefore the
term listen used instead. More information about ListenBrainz is
available at listenbrainz [dot] org.
---
 modules/misc/Makefile.am    |   4 +
 modules/misc/listenbrainz.c | 537 ++++++++++++++++++++++++++++++++++++
 2 files changed, 541 insertions(+)
 create mode 100755 modules/misc/listenbrainz.c

diff --git a/modules/misc/Makefile.am b/modules/misc/Makefile.am
index 78f9b09710..ed3ef24ee6 100644
--- a/modules/misc/Makefile.am
+++ b/modules/misc/Makefile.am
@@ -8,6 +8,10 @@ libaudioscrobbler_plugin_la_SOURCES = misc/audioscrobbler.c
 libaudioscrobbler_plugin_la_LIBADD = $(SOCKET_LIBS)
 misc_LTLIBRARIES += libaudioscrobbler_plugin.la
 
+liblistenbrainz_plugin_la_SOURCES = misc/listenbrainz.c
+liblistenbrainz_plugin_la_LIBADD = $(SOCKET_LIBS)
+misc_LTLIBRARIES += liblistenbrainz_plugin.la
+
 libexport_plugin_la_SOURCES = \
 	misc/playlist/html.c \
 	misc/playlist/m3u.c \
diff --git a/modules/misc/listenbrainz.c b/modules/misc/listenbrainz.c
new file mode 100755
index 0000000000..a000bcc6ac
--- /dev/null
+++ b/modules/misc/listenbrainz.c
@@ -0,0 +1,537 @@
+/*****************************************************************************
+ * listenbrainz.c : ListenBrainz submission plugin
+ * ListenBrainz Submit Listens API 1
+ * https://api.listenbrainz.org/1/submit-listens
+ *****************************************************************************
+ * Author: Kartik Ohri <kartikohri13 at gmail dot com>
+ *****************************************************************************/
+
+#ifdef HAVE_CONFIG_H
+# include "config.h"
+#endif
+
+#include <assert.h>
+#include <time.h>
+
+#include <vlc_common.h>
+#include <vlc_plugin.h>
+#include <vlc_interface.h>
+#include <vlc_input_item.h>
+#include <vlc_dialog.h>
+#include <vlc_meta.h>
+#include <vlc_memstream.h>
+#include <vlc_stream.h>
+#include <vlc_url.h>
+#include <vlc_tls.h>
+#include <vlc_player.h>
+#include <vlc_playlist.h>
+
+#define CAPACITY 50
+
+typedef struct listen_t
+{
+    char *psz_artist;
+    char *psz_title;
+    char *psz_album;
+    char *psz_track_number;
+    int i_length;
+    char *psz_musicbrainz_id;
+    time_t date;
+    vlc_tick_t time_start;
+} listen_t;
+
+struct intf_sys_t
+{
+
+    listen_t p_queue[CAPACITY];
+    int i_songs;                    // number of songs
+
+    vlc_playlist_t *playlist;
+    struct vlc_playlist_listener_id *playlist_listener;
+    struct vlc_player_listener_id *player_listener;
+
+    vlc_mutex_t lock;
+    vlc_cond_t wait;                // song to submit event
+    vlc_thread_t thread;            // thread to submit song
+
+    vlc_url_t p_submit_url;         // where to submit data
+    char *psz_user_token;           // authentication token
+
+
+    listen_t p_current_song;
+    bool b_meta_read;               // check if song metadata is already read
+
+    vlc_tick_t time_pause;          // time when vlc paused
+    vlc_tick_t time_total_pauses;   // total time in pause
+
+};
+
+static int Open (vlc_object_t *);
+static void Close (vlc_object_t *);
+static void *Run (void *);
+
+#define USER_TOKEN_TEXT      N_("User token")
+#define USER_TOKEN_LONGTEXT  N_("The user token of your ListenBrainz account")
+#define URL_TEXT             N_("Submission URL")
+#define URL_LONGTEXT         N_("The URL set for an alternative ListenBrainz instance")
+
+/****************************************************************************
+ * Module descriptor
+ ****************************************************************************/
+
+vlc_module_begin ()
+    set_category(CAT_INTERFACE)
+    set_subcategory(SUBCAT_INTERFACE_CONTROL)
+    set_shortname (N_ ("ListenBrainz"))
+    set_description (N_ ("Submit listens to ListenBrainz"))
+    add_string("listenbrainz_user_token", "", USER_TOKEN_TEXT, USER_TOKEN_LONGTEXT, false)
+    add_string("listenbrainz_submission_url", "api.listenbrainz.org", URL_TEXT, URL_LONGTEXT, false)
+    set_capability("interface", 0)
+    set_callbacks(Open, Close)
+vlc_module_end ()
+
+static void DeleteSong (listen_t *p_song)
+{
+    p_song->psz_artist = NULL;
+    p_song->psz_album = NULL;
+    p_song->psz_title = NULL;
+    p_song->psz_musicbrainz_id = NULL;
+    p_song->psz_track_number = NULL;
+}
+
+static void ReadMetaData (intf_thread_t *p_this)
+{
+    bool b_skip = 0;
+    intf_sys_t *p_sys = p_this->p_sys;
+
+    vlc_player_t *player = vlc_playlist_GetPlayer (p_sys->playlist);
+    input_item_t *item = vlc_player_GetCurrentMedia (player);
+    if ( item == NULL )
+        return;
+
+    vlc_mutex_lock (&p_sys->lock);
+
+    p_sys->b_meta_read = true;
+
+#define RETRIEVE_METADATA(a, b) do { \
+        char *psz_data = input_item_Get##b(item); \
+        if (psz_data && *psz_data) \
+            a = vlc_uri_encode(psz_data); \
+        free(psz_data); \
+    } while (0)
+
+    RETRIEVE_METADATA(p_sys->p_current_song.psz_artist, Artist);
+    if ( !p_sys->p_current_song.psz_artist )
+    {
+        msg_Dbg (p_this, "Artist missing.");
+        DeleteSong (&p_sys->p_current_song);
+        b_skip = 1;
+    }
+
+    RETRIEVE_METADATA(p_sys->p_current_song.psz_title, Title);
+    if ( b_skip || !p_sys->p_current_song.psz_title )
+    {
+        msg_Dbg (p_this, "Track name missing.");
+        DeleteSong (&p_sys->p_current_song);
+        b_skip = 1;
+    }
+
+    if ( !b_skip )
+    {
+        RETRIEVE_METADATA(p_sys->p_current_song.psz_album, Album);
+        RETRIEVE_METADATA(p_sys->p_current_song.psz_musicbrainz_id, TrackID);
+        RETRIEVE_METADATA(p_sys->p_current_song.psz_track_number, TrackNum);
+        p_sys->p_current_song.i_length = SEC_FROM_VLC_TICK (input_item_GetDuration (item));
+        msg_Dbg (p_this, "Meta data registered");
+        vlc_cond_signal (&p_sys->wait);
+    }
+    vlc_mutex_unlock (&p_sys->lock);
+
+#undef RETRIEVE_METADATA
+
+}
+
+static void Enqueue (intf_thread_t *p_this)
+{
+    bool b_skip = 0;
+    int64_t i_played_time;
+    intf_sys_t *p_sys = p_this->p_sys;
+
+    vlc_mutex_lock (&p_sys->lock);
+
+    if ( !p_sys->p_current_song.psz_artist || !*p_sys->p_current_song.psz_artist ||
+         !p_sys->p_current_song.psz_title || !*p_sys->p_current_song.psz_title )
+    {
+        msg_Dbg (p_this, "Missing artist or title, not submitting");
+        b_skip = 1;
+    }
+
+    i_played_time = SEC_FROM_VLC_TICK (vlc_tick_now () - p_sys->p_current_song.time_start - p_sys->time_total_pauses);
+
+    if ( p_sys->p_current_song.i_length == 0 )
+        p_sys->p_current_song.i_length = i_played_time;
+
+    if ( !b_skip && i_played_time < 30 )
+    {
+        msg_Dbg (p_this, "Song not listened long enough, not submitting");
+        b_skip = 1;
+    }
+
+    if ( !b_skip && p_sys->i_songs >= CAPACITY )
+    {
+        msg_Warn (p_this, "Submission queue is full, not submitting");
+        b_skip = 1;
+    }
+
+    if ( !b_skip )
+    {
+        msg_Dbg (p_this, "Song will be submitted.");
+
+        p_sys->p_queue[p_sys->i_songs].psz_artist = p_sys->p_current_song.psz_artist;
+        p_sys->p_queue[p_sys->i_songs].psz_title = p_sys->p_current_song.psz_title;
+        p_sys->p_queue[p_sys->i_songs].psz_album = p_sys->p_current_song.psz_album;
+        p_sys->p_queue[p_sys->i_songs].psz_musicbrainz_id = p_sys->p_current_song.psz_musicbrainz_id;
+        p_sys->p_queue[p_sys->i_songs].psz_track_number = p_sys->p_current_song.psz_track_number;
+        p_sys->p_queue[p_sys->i_songs].i_length = p_sys->p_current_song.i_length;
+        p_sys->p_queue[p_sys->i_songs].date = p_sys->p_current_song.date;
+
+        p_sys->i_songs++;
+    }
+
+    vlc_cond_signal (&p_sys->wait);
+    DeleteSong (&p_sys->p_current_song);
+    vlc_mutex_unlock (&p_sys->lock);
+}
+
+static void PlayerStateChanged (vlc_player_t *player, enum vlc_player_state state, void *data)
+{
+    intf_thread_t *intf = data;
+    intf_sys_t *p_sys = intf->p_sys;
+
+    if ( vlc_player_GetVideoTrackCount (player) )
+    {
+        msg_Dbg (intf, "Not an audio-only input, not submitting");
+        return;
+    }
+
+    if ( !p_sys->b_meta_read && state >= VLC_PLAYER_STATE_PLAYING )
+    {
+        ReadMetaData (intf);
+        return;
+    }
+
+    switch (state)
+    {
+        case VLC_PLAYER_STATE_STOPPED:
+            Enqueue (intf);
+            break;
+        case VLC_PLAYER_STATE_PAUSED:
+            p_sys->time_pause = vlc_tick_now ();
+            break;
+        case VLC_PLAYER_STATE_PLAYING:
+            if ( p_sys->time_pause > 0 )
+            {
+                vlc_tick_t time_current = vlc_tick_now ();
+                vlc_tick_t time_paused = time_current - p_sys->time_pause;
+                p_sys->time_total_pauses += time_paused;
+
+                // If pause duration more than 60s, check for if played part qualifies for individual listen
+                if ( SEC_FROM_VLC_TICK (time_paused) > 60 )
+                {
+                    int64_t i_played_time = SEC_FROM_VLC_TICK (
+                            time_current - p_sys->p_current_song.time_start - p_sys->time_total_pauses);
+
+                    // check whether the item as of now qualifies as a listen
+                    if ( i_played_time > 30 )
+                    {
+                        Enqueue (intf);
+                        ReadMetaData (intf); // Enqueueing deletes the current song so reset the song for the next listen.
+                        p_sys->p_current_song.time_start = time_current;
+                        time (&p_sys->p_current_song.date);
+                        p_sys->time_total_pauses = 0;
+                    }
+                }
+                p_sys->time_pause = 0;
+            }
+            break;
+        default:
+            break;
+    }
+}
+
+static void PlaylistItemChanged (vlc_playlist_t *playlist, ssize_t index, void *data)
+{
+    VLC_UNUSED (index);
+
+    intf_thread_t *intf = data;
+    if ( index > 0 )
+        Enqueue (intf);
+
+    intf_sys_t *p_sys = intf->p_sys;
+    p_sys->b_meta_read = false;
+
+    vlc_player_t *player = vlc_playlist_GetPlayer (playlist);
+    input_item_t *item = vlc_player_GetCurrentMedia (player);
+
+    if ( !item || vlc_player_GetVideoTrackCount (player) )
+    {
+        msg_Dbg (intf, "Invalid item or not an audio-only input.");
+        return;
+    }
+
+    p_sys->time_total_pauses = 0;
+    time (&p_sys->p_current_song.date);                   // time sent to ListenBrainz
+    p_sys->p_current_song.time_start = vlc_tick_now ();   // time used locally to check duration of play
+
+    if ( input_item_IsPreparsed (item) )
+        ReadMetaData (intf);
+}
+
+static int Open (vlc_object_t *p_this)
+{
+
+    intf_thread_t *p_intf = (intf_thread_t *) p_this;
+    intf_sys_t *p_sys = calloc (1, sizeof (intf_sys_t));
+    bool b_fail = 0;
+
+    if ( !p_sys )
+        return VLC_ENOMEM;
+
+    p_intf->p_sys = p_sys;
+
+    static struct vlc_playlist_callbacks const playlist_cbs =
+            {
+                    .on_current_index_changed = PlaylistItemChanged,
+            };
+    static struct vlc_player_cbs const player_cbs =
+            {
+                    .on_state_changed = PlayerStateChanged,
+            };
+
+    vlc_playlist_t *playlist = p_sys->playlist = vlc_intf_GetMainPlaylist (p_intf);
+    vlc_player_t *player = vlc_playlist_GetPlayer (playlist);
+
+    vlc_playlist_Lock (playlist);
+    p_sys->playlist_listener = vlc_playlist_AddListener (playlist, &playlist_cbs, p_intf, false);
+    if ( !p_sys->playlist_listener )
+    {
+        vlc_playlist_Unlock (playlist);
+        b_fail = 1;
+    } else
+    {
+        p_sys->player_listener = vlc_player_AddListener (player, &player_cbs, p_intf);
+        vlc_playlist_Unlock (playlist);
+        if ( !p_sys->player_listener )
+            b_fail = 1;
+    }
+    if ( !b_fail )
+    {
+        vlc_mutex_init (&p_sys->lock);
+        vlc_cond_init (&p_sys->wait);
+
+        if ( vlc_clone (&p_sys->thread, Run, p_intf, VLC_THREAD_PRIORITY_LOW) )
+            b_fail = 1;
+    }
+    if ( b_fail )
+    {
+        if ( p_sys->playlist_listener )
+        {
+            vlc_playlist_Lock (playlist);
+            if ( p_sys->player_listener )
+                vlc_player_RemoveListener (player, p_sys->player_listener);
+            vlc_playlist_RemoveListener (playlist, p_sys->playlist_listener);
+            vlc_playlist_Unlock (playlist);
+        }
+        free (p_sys);
+        return VLC_EGENERIC;
+    }
+    return VLC_SUCCESS;
+}
+
+static void Close (vlc_object_t *p_this)
+{
+    intf_thread_t *p_intf = (intf_thread_t *) p_this;
+    intf_sys_t *p_sys = p_intf->p_sys;
+    vlc_playlist_t *playlist = p_sys->playlist;
+
+    vlc_cancel (p_sys->thread);
+    vlc_join (p_sys->thread, NULL);
+
+    int i;
+    for ( i = 0; i < p_sys->i_songs; i++ )
+        DeleteSong (&p_sys->p_queue[i]);
+    vlc_UrlClean (&p_sys->p_submit_url);
+
+    vlc_playlist_Lock (playlist);
+    vlc_player_RemoveListener (vlc_playlist_GetPlayer (playlist), p_sys->player_listener);
+    vlc_playlist_RemoveListener (playlist, p_sys->playlist_listener);
+    vlc_playlist_Unlock (playlist);
+
+    free (p_sys);
+}
+
+static void *Run (void *data)
+{
+    intf_thread_t *p_intf = data;
+    uint8_t p_buffer[1024];
+    int canc = vlc_savecancel ();
+    char *psz_url, *psz_submission_url;
+    int i_ret;
+    bool b_wait = 1;
+
+    intf_sys_t *p_sys = p_intf->p_sys;
+
+    while ( 1 )
+    {
+        vlc_restorecancel (canc);
+        if ( b_wait )
+            vlc_tick_wait (vlc_tick_now () + VLC_TICK_FROM_SEC (60)); // wait for 1 min
+
+        vlc_mutex_lock (&p_sys->lock);
+        mutex_cleanup_push (&p_sys->lock) ;
+
+        while ( p_sys->i_songs == 0 )
+            vlc_cond_wait (&p_sys->wait, &p_sys->lock);
+
+        msg_Dbg (p_intf, "Waiting Over");
+        vlc_cleanup_pop ();
+        vlc_mutex_unlock (&p_sys->lock);
+        canc = vlc_savecancel ();
+
+        p_sys->psz_user_token = var_InheritString (p_intf, "listenbrainz_user_token");
+        msg_Dbg (p_intf, "Begin");
+
+        if ( EMPTY_STR (p_sys->psz_user_token) )
+        {
+            free (p_sys->psz_user_token);
+            vlc_dialog_display_error (p_intf,
+                                      _ ("Listenbrainz User Token not set"), "%s",
+                                      _ ("Please set a user token or disable the ListenBrainz plugin, and restart VLC.\n"
+                                         " Visit https://listenbrainz.org/profile/ to get a user token."));
+            break;
+        }
+
+        psz_submission_url = var_InheritString (p_intf, "listenbrainz_submission_url");
+        if ( !psz_submission_url )
+            break;
+        msg_Dbg (p_intf, "Submission URL Retrieved");
+
+        i_ret = asprintf (&psz_url, "https://%s/1/submit-listens", psz_submission_url);
+        free (psz_submission_url);
+        if ( i_ret == -1 )
+            break;
+        msg_Dbg (p_intf, "Submission URL Parsed");
+
+        vlc_UrlParse (&p_sys->p_submit_url, psz_url);
+        free (psz_url);
+        msg_Dbg (p_intf, "Submit data");
+
+        vlc_url_t *url;
+        struct vlc_memstream req, payload;
+        vlc_memstream_open (&payload);
+
+        vlc_mutex_lock (&p_sys->lock);
+
+        url = &p_sys->p_submit_url;
+        b_wait = 0;
+
+        if ( p_sys->i_songs == 1 )
+            vlc_memstream_printf (&payload, "{\"listen_type\":\"single\",\"payload\":[");
+        else
+            vlc_memstream_printf (&payload, "{\"listen_type\":\"import\",\"payload\":[");
+
+        for ( int i_song = 0; i_song < p_sys->i_songs; i_song++ )
+        {
+            listen_t *p_song = &p_sys->p_queue[i_song];
+
+            vlc_memstream_printf (&payload, "{\"listened_at\": %"
+                                            PRIu64, (uint64_t) p_song->date);
+            vlc_memstream_printf (&payload, ", \"track_metadata\": {\"artist_name\": \"%s\", ",
+                                  vlc_uri_decode (p_song->psz_artist));
+            vlc_memstream_printf (&payload, " \"track_name\": \"%s\", ", vlc_uri_decode (p_song->psz_title));
+            if ( p_song->psz_album != NULL )
+                vlc_memstream_printf (&payload, " \"release_name\": \"%s\"", vlc_uri_decode (p_song->psz_album));
+            if ( p_song->psz_musicbrainz_id != NULL )
+                vlc_memstream_printf (&payload, ", \"additional_info\": {\"recording_mbid\":\"%s\"} ",
+                                      vlc_uri_decode (p_song->psz_musicbrainz_id));
+            vlc_memstream_printf (&payload, "}}");
+        }
+
+        vlc_memstream_printf (&payload, "]}");
+        vlc_mutex_unlock (&p_sys->lock);
+
+        if ( vlc_memstream_close (&payload) )
+            break;
+
+        vlc_memstream_open (&req);
+        vlc_memstream_printf (&req, "POST %s HTTP/1.1\r\n", url->psz_path);
+        vlc_memstream_printf (&req, "Host: %s\r\n", url->psz_host);
+        vlc_memstream_printf (&req, "Authorization: Token %s\r\n", p_sys->psz_user_token);
+        vlc_memstream_puts (&req, "User-Agent: "PACKAGE"/"VERSION"\r\n");
+        vlc_memstream_puts (&req, "Connection: close\r\n");
+        vlc_memstream_puts (&req, "Accept-Encoding: identity\r\n");
+        vlc_memstream_printf (&req, "Content-Length: %zu\r\n", payload.length);
+        vlc_memstream_puts (&req, "\r\n");
+        vlc_memstream_write (&req, payload.ptr, payload.length);
+        vlc_memstream_puts (&req, "\r\n\r\n");
+
+        free (payload.ptr);
+
+        if ( vlc_memstream_close (&req) )
+            break;
+
+        msg_Dbg (p_intf, "%s", req.ptr);
+        msg_Dbg (p_intf, "Open socket");
+        vlc_tls_client_t *creds = vlc_tls_ClientCreate (VLC_OBJECT (p_intf));
+        vlc_tls_t *sock = vlc_tls_SocketOpenTLS (creds, url->psz_host, 443, NULL, NULL, NULL);
+
+        if ( sock == NULL )
+        {
+            b_wait = 1;
+            free (req.ptr);
+            continue;
+        }
+
+        msg_Warn (p_intf, "Begin transmission");
+        i_ret = vlc_tls_Write (sock, req.ptr, req.length);
+        msg_Warn (p_intf, "Transmission End");
+        free (req.ptr);
+
+        if ( i_ret == -1 )
+        {
+            b_wait = 1;
+            vlc_tls_Close (sock);
+            msg_Dbg (p_intf, "Close socket");
+            continue;
+        }
+
+        msg_Warn (p_intf, "Checking response");
+        i_ret = vlc_tls_Read (sock, p_buffer, sizeof (p_buffer) - 1, false);
+        msg_Warn (p_intf, "Response: %s", (char *) p_buffer);
+        vlc_tls_Close (sock);
+        if ( i_ret <= 0 )
+        {
+            msg_Warn (p_intf, "No response");
+            continue;
+        }
+        p_buffer[i_ret] = '\0';
+        if ( strstr ((char *) p_buffer, "OK") )
+        {
+            for ( int i = 0; i < p_sys->i_songs; i++ )
+                DeleteSong (&p_sys->p_queue[i]);
+            p_sys->i_songs = 0;
+
+            b_wait = 1;
+            msg_Dbg (p_intf, "Submission successful!");
+        } else
+        {
+            msg_Warn (p_intf, "Error: %s", (char *) p_buffer);
+            b_wait = 1;
+            continue;
+        }
+    }
+
+    vlc_restorecancel (canc);
+    return NULL;
+}
+
-- 
2.20.1



More information about the vlc-devel mailing list