[vlc-devel] [PATCH] Add module to submit listens to ListenBrainz
Thomas Guillem
thomas at gllm.fr
Tue Mar 3 21:49:12 CET 2020
On Tue, Mar 3, 2020, at 20:05, Kartik Ohri kartikohri13 at gmail.com wrote:
> 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.
Hello,
Thanks a lot for this rewrite.
cf. my coments below:
> ---
> 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
Missing license header, you can take it from any LGPL file.
> +
> *****************************************************************************
> + * 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
use vlc_vector instead of limiting the capacity.
> +
> +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
> +{
> +
stray line
> + 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
> +
> +
stray line
> + 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)
options name use generally '-' instead of '_'.
> + 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;
You are missing some free here.
RETRIEVE_METADATA return an allocated str.
> +}
> +
> +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;
YOu should listen to the player time event instead. Cf. the vlc_player_timer API.
> + }
> + 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
Why are you waiting for 1min here ?
> +
> + 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;
> + }
It should be checked from Open() instead (and return VLC_EGENERIC from Open() in that case).
> +
> + 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;
> + }
YOu should split the Run function into several one:
- prepare payload
- prepare http request
- tls/http post
- etc...
> + }
> +
> + vlc_restorecancel (canc);
> + return NULL;
> +}
> +
> --
> 2.20.1
>
> _______________________________________________
> vlc-devel mailing list
> To unsubscribe or modify your subscription options:
> https://mailman.videolan.org/listinfo/vlc-devel
More information about the vlc-devel
mailing list