[vlc-devel] [PATCH] Add module to submit listens to ListenBrainz.
Thomas Guillem
thomas at gllm.fr
Mon Apr 20 14:31:45 CEST 2020
Sorry for the very long delay,
On Wed, Apr 8, 2020, at 15:58, Kartik Ohri 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.
> ---
> modules/misc/Makefile.am | 4 +
> modules/misc/listenbrainz.c | 577 ++++++++++++++++++++++++++++++++++++
> 2 files changed, 581 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..5422af9b49
> --- /dev/null
> +++ b/modules/misc/listenbrainz.c
> @@ -0,0 +1,577 @@
> +/*****************************************************************************
> + * listenbrainz.c : ListenBrainz submission plugin
> + * ListenBrainz Submit Listens API 1
> + * https://api.listenbrainz.org/1/submit-listens
> +
> *****************************************************************************
> + * Copyright (C) 2020 VLC authors and VideoLAN
> + *
> + * Author: Kartik Ohri <kartikohri13 at gmail dot com>
> + *
> + * This program is free software; you can redistribute it and/or
> modify it
> + * under the terms of the GNU Lesser General Public License as
> published by
> + * the Free Software Foundation; either version 2.1 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 Lesser General Public License for more details.
> + *
> + * You should have received a copy of the GNU Lesser General Public
> License
> + * along with this program; if not, write to the Free Software
> Foundation,
> + * Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
> +
> *****************************************************************************/
> +
> +#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>
> +#include <vlc_vector.h>
> +#include <vlc_interrupt.h>
> +
> +typedef struct listen_t {
> + char *psz_artist;
> + char *psz_title;
> + char *psz_album;
> + char *psz_track_number;
> + char *psz_musicbrainz_recording_id;
> + int i_length;
> + time_t date;
> +} listen_t;
> +
> +typedef struct VLC_VECTOR (listen_t) vlc_vector_listen_t;
> +
> +struct intf_sys_t {
> + vlc_vector_listen_t queue;
> +
> + vlc_player_t *player;
> + struct vlc_player_listener_id *player_listener;
> + struct vlc_player_timer_id *timer_listener;
> +
> + vlc_mutex_t lock;
> + vlc_cond_t wait; // song to submit event
> + vlc_thread_t thread; // thread to submit song
> + vlc_interrupt_t *interrupt;
> + bool live;
> +
> + 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_played;
> +};
> +
> +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")
> +
> +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)
> +{
> + FREENULL (p_song->psz_artist);
> + FREENULL (p_song->psz_album);
> + FREENULL (p_song->psz_title);
> + FREENULL (p_song->psz_musicbrainz_recording_id);
> + FREENULL (p_song->psz_track_number);
> + p_song->i_length = 0;
> + p_song->date = 0;
> +}
> +
> +static void DeleteSongQueue(intf_thread_t *p_this)
> +{
> + intf_sys_t *p_sys = p_this->p_sys;
> +
> + for (size_t i = 0; i < p_sys->queue.size; ++i)
> + DeleteSong(&p_sys->queue.data[i]);
> + vlc_vector_clear (&p_sys->queue);
> +}
> +
> +static void ReadMetaData (intf_thread_t *p_this , input_item_t *item)
> +{
> + intf_sys_t *p_sys = p_this->p_sys;
> +
> + if (item == NULL)
> + return;
> +
> + vlc_mutex_lock (&p_sys->lock);
> +
> + p_sys->b_meta_read = true;
> + time (&p_sys->p_current_song.date);
> +
> +/* The retrieved metadata is encoded and then decoded to avoid UTF
> errors
> + * while sending the JSON payload.*/
> +#define RETRIEVE_METADATA(a , b) do {
> \
> + char *psz_data = input_item_Get##b(item);
> \
> + if (psz_data && *psz_data) {
> \
> + free(a);
> \
> + a = vlc_uri_decode(vlc_uri_encode(psz_data));
You don't check if vlc_uri_encode() return is NULL.
BTW, in VLC, we prefer to not chain multiple functions calls in one line.
Also, you need to explain why you need that. Why not just calling vlc_uri_decode() ?
> \
> + }
> \
> + free(psz_data);
> \
> + } while (0)
> +
> + RETRIEVE_METADATA(p_sys->p_current_song.psz_artist , AlbumArtist);
> + if (!p_sys->p_current_song.psz_artist)
> + {
> + RETRIEVE_METADATA(p_sys->p_current_song.psz_artist , Artist);
> + if (!p_sys->p_current_song.psz_artist)
> + {
> + DeleteSong (&p_sys->p_current_song);
> + goto error;
> + }
> + }
> +
> + RETRIEVE_METADATA(p_sys->p_current_song.psz_title , Title);
> + if (!p_sys->p_current_song.psz_title)
> + {
> + DeleteSong (&p_sys->p_current_song);
> + goto error;
> + }
> +
> + RETRIEVE_METADATA(p_sys->p_current_song.psz_album , Album);
> +
> RETRIEVE_METADATA(p_sys->p_current_song.psz_musicbrainz_recording_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);
> +
> +error:
> + vlc_mutex_unlock (&p_sys->lock);
> +
> +#undef RETRIEVE_METADATA
> +}
> +
> +static void Enqueue (intf_thread_t *p_this)
> +{
> + intf_sys_t *p_sys = p_this->p_sys;
> +
> + p_sys->b_meta_read = false;
> + /* Song not yet initialized */
> + if(p_sys->p_current_song.date == 0)
> + return;
> + vlc_mutex_lock (&p_sys->lock);
> +
> + if (EMPTY_STR(p_sys->p_current_song.psz_artist) ||
> + EMPTY_STR(p_sys->p_current_song.psz_title))
> + {
> + msg_Dbg (p_this , "Missing artist or title, not submitting");
> + goto error;
> + }
> +
> + if (p_sys->p_current_song.i_length == 0)
> + p_sys->p_current_song.i_length = p_sys->time_played;
> +
> + if (p_sys->time_played < 30)
> + {
> + msg_Dbg (p_this , "Song not listened long enough, not
> submitting");
> + goto error;
> + }
> +
> + msg_Dbg (p_this , "Song will be submitted.");
> + /* Transfer the ownership of allocated datas to the queue */
> + vlc_vector_push (&p_sys->queue , p_sys->p_current_song);
> + memset(&p_sys->p_current_song, 0, sizeof(p_sys->p_current_song));
> +
> + vlc_cond_signal (&p_sys->wait);
> + vlc_mutex_unlock (&p_sys->lock);
> + return;
> +
> +error:
> + 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))
> + return;
> +
> + if (!p_sys->b_meta_read && state >= VLC_PLAYER_STATE_PLAYING)
> + {
> + input_item_t *item = vlc_player_GetCurrentMedia
> (p_sys->player);
> + ReadMetaData (intf , item);
> + return;
> + }
> +
> + if (state == VLC_PLAYER_STATE_STOPPED)
> + Enqueue (intf);
> +}
> +
> +static void OnTimerUpdate (const struct vlc_player_timer_point *value
> , void *data)
> +{
> + intf_thread_t *intf = data;
> + intf_sys_t *p_sys = intf->p_sys;
> + p_sys->time_played = SEC_FROM_VLC_TICK (value->ts - VLC_TICK_0);
> +}
> +
> +static void OnTimerStopped (vlc_tick_t system_date , void *data)
> +{
> + (void) system_date;
> + (void) data;
> +}
> +
> +static void OnCurrentMediaChanged (vlc_player_t *player , input_item_t
> *new_media , void *data)
> +{
> + intf_thread_t *intf = data;
> + Enqueue (intf);
> +
> + intf_sys_t *p_sys = intf->p_sys;
> + p_sys->b_meta_read = false;
> +
> + if (!new_media || vlc_player_GetVideoTrackCount (player))
> + return;
> +
> + p_sys->time_played = 0;
> + if (input_item_IsPreparsed (new_media))
> + ReadMetaData (intf , new_media);
> +}
> +
> +static char *PreparePayload (intf_thread_t *p_this)
> +{
> + intf_sys_t *p_sys = p_this->p_sys;
> + struct vlc_memstream payload;
> + vlc_memstream_open (&payload);
> +
> + if (p_sys->queue.size == 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 < ( int ) p_sys->queue.size ;
> i_song++)
> + {
> + listen_t *p_song = &p_sys->queue.data[ i_song ];
> +
> + vlc_memstream_printf (&payload , "{\"listened_at\": %"PRIu64 ,
> ( uint64_t ) p_song->date);
> + vlc_memstream_printf (&payload , ", \"track_metadata\":
> {\"artist_name\": \"%s\", " ,
> + p_song->psz_artist);
> + vlc_memstream_printf (&payload , " \"track_name\": \"%s\", " ,
> p_song->psz_title);
> + if (!EMPTY_STR (p_song->psz_album))
> + vlc_memstream_printf (&payload , " \"release_name\":
> \"%s\"" , p_song->psz_album);
> + if (!EMPTY_STR (p_song->psz_musicbrainz_recording_id))
> + vlc_memstream_printf (&payload , ", \"additional_info\":
> {\"recording_mbid\":\"%s\"} " ,
> +
> p_song->psz_musicbrainz_recording_id);
> + vlc_memstream_printf (&payload , "}}");
> + }
> +
> + vlc_memstream_printf (&payload , "]}");
> +
> + int i_status = vlc_memstream_close (&payload);
> + if (!i_status)
> + {
> + msg_Dbg (p_this , "Payload: %s" , payload.ptr);
> + return payload.ptr;
> + }
> + else
> + return NULL;
> +}
> +
> +static char *PrepareRequest (intf_thread_t *p_this , char *payload)
> +{
> + intf_sys_t *p_sys = p_this->p_sys;
> + struct vlc_memstream request;
> +
> + vlc_memstream_open (&request);
> + vlc_memstream_printf (&request , "POST %s HTTP/1.1\r\n" ,
> p_sys->p_submit_url.psz_path);
> + vlc_memstream_printf (&request , "Host: %s\r\n" ,
> p_sys->p_submit_url.psz_host);
> + vlc_memstream_printf (&request , "Authorization: Token %s\r\n" ,
> p_sys->psz_user_token);
> + vlc_memstream_puts (&request , "User-Agent:
> "PACKAGE"/"VERSION"\r\n");
> + vlc_memstream_puts (&request , "Connection: close\r\n");
> + vlc_memstream_puts (&request , "Accept-Encoding: identity\r\n");
> + vlc_memstream_printf (&request , "Content-Length: %zu\r\n" ,
> strlen (payload));
> + vlc_memstream_puts (&request , "\r\n");
> + vlc_memstream_puts (&request , payload);
> + vlc_memstream_puts (&request , "\r\n\r\n");
> +
> + free (payload);
> +
> + int i_status = vlc_memstream_close (&request);
> + if (!i_status)
> + return request.ptr;
> + else
> + return NULL;
> +}
> +
> +static int SendRequest (intf_thread_t *p_this , char *request)
> +{
> + uint8_t p_buffer[1024];
> + int i_ret;
> +
> + intf_sys_t *p_sys = p_this->p_sys;
> + vlc_tls_client_t *creds = vlc_tls_ClientCreate (VLC_OBJECT
> (p_this));
> + vlc_tls_t *sock = vlc_tls_SocketOpenTLS (creds ,
> p_sys->p_submit_url.psz_host , 443 , NULL , NULL , NULL);
> +
> + if (sock == NULL)
> + {
> + vlc_tls_ClientDelete(creds);
> + return VLC_EGENERIC;
> + }
> +
> + i_ret = vlc_tls_Write (sock , request , strlen (request));
> +
> + if (i_ret == -1)
> + {
> + vlc_tls_Close (sock);
> + vlc_tls_ClientDelete(creds);
> + return VLC_EGENERIC;
> + }
> +
> + i_ret = vlc_tls_Read (sock , p_buffer , sizeof (p_buffer) - 1 ,
> false);
> + msg_Dbg (p_this , "Response: %s" , ( char * ) p_buffer);
> + vlc_tls_Close (sock);
> + vlc_tls_ClientDelete(creds);
> + if (i_ret <= 0)
> + {
> + msg_Warn (p_this , "No response");
> + return VLC_EGENERIC;
> + }
> + p_buffer[ i_ret ] = '\0';
> +
> + char *status = strchr((char *)p_buffer, '\n');
> + *status = 0;
You don't check if status is NULL. Never forget the number 1 rule when parsing data: never trust the input.
I think you could also check that the line starts with "HTTP/1".
> +
> + if (strstr (( char * ) p_buffer , "200"))
> + {
> + msg_Dbg (p_this , "Submission successful!");
> + return VLC_SUCCESS;
> + }
> + else if (strstr (( char * ) p_buffer , "401"))
> + msg_Warn (p_this , "Authentication Error");
> + else
> + msg_Warn (p_this , "Invalid Request");
> +
> + return VLC_EGENERIC;
> +}
> +
> +static int Configure (intf_thread_t *p_intf)
> +{
> + int i_ret;
> + char *psz_submission_url , *psz_url;
> + intf_sys_t *p_sys = p_intf->p_sys;
> +
> + p_sys->psz_user_token = var_InheritString (p_intf ,
> "listenbrainz-user-token");
> + if (EMPTY_STR (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."));
> + return VLC_EGENERIC;
> + }
> +
> + psz_submission_url = var_InheritString (p_intf ,
> "listenbrainz-submission-url");
> + if (psz_submission_url)
> + {
> + i_ret = asprintf (&psz_url , "https://%s/1/submit-listens" ,
> psz_submission_url);
> + free (psz_submission_url);
> + if (i_ret != -1)
> + {
> + vlc_UrlParse (&p_sys->p_submit_url , psz_url);
> + free (psz_url);
> + return VLC_SUCCESS;
> + }
> + }
> +
> + vlc_dialog_display_error (p_intf ,
> + _ ("ListenBrainz API URL Invalid") ,
> "%s" ,
> + _ ("Please set a valid endpoint URL. The
> default value is api.listenbrainz.org ."));
> + return VLC_EGENERIC;
> +}
> +
> +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));
> +
> + if (!p_sys)
> + return VLC_ENOMEM;
> +
> + p_intf->p_sys = p_sys;
> + p_sys->live = true;
> +
> + if (Configure (p_intf) != VLC_SUCCESS)
> + goto error;
> +
> + static struct vlc_player_cbs const player_cbs =
> + {
> + .on_state_changed = PlayerStateChanged ,
> + .on_current_media_changed = OnCurrentMediaChanged ,
> + };
> + static struct vlc_player_timer_cbs const timer_cbs =
> + {
> + .on_update = OnTimerUpdate ,
> + .on_discontinuity = OnTimerStopped ,
> + };
> +
> + vlc_playlist_t *playlist = vlc_intf_GetMainPlaylist (p_intf);
> + p_sys->player = vlc_playlist_GetPlayer (playlist);
> +
> + vlc_player_Lock (p_sys->player);
> + p_sys->player_listener = vlc_player_AddListener (p_sys->player ,
> &player_cbs , p_intf);
> + vlc_player_Unlock (p_sys->player);
> +
> + if (!p_sys->player_listener)
> + goto error;
> +
> + p_sys->timer_listener = vlc_player_AddTimer (p_sys->player ,
> VLC_TICK_FROM_SEC (1) , &timer_cbs , p_intf);
> + if (!p_sys->timer_listener)
> + goto error;
> +
> + p_sys->interrupt = vlc_interrupt_create();
> + if (unlikely(p_sys->interrupt == NULL))
> + goto error;
> +
> + 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))
> + {
> + vlc_interrupt_destroy(p_sys->interrupt);
> + goto error;
> + }
> +
> + vlc_vector_init(&p_sys->queue);
> + return VLC_SUCCESS;
> +
> +error:
> + if (p_sys->player_listener)
> + {
> + vlc_player_Lock (p_sys->player);
> + vlc_player_RemoveListener (p_sys->player ,
> p_sys->player_listener);
> + vlc_player_Unlock (p_sys->player);
> + }
> + if (p_sys->timer_listener)
> + vlc_player_RemoveTimer (p_sys->player , p_sys->timer_listener);
> + vlc_UrlClean (&p_sys->p_submit_url);
> + free(p_sys->psz_user_token);
> + free (p_sys);
> + return VLC_EGENERIC;
> +}
> +
> +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_mutex_lock(&p_sys->lock);
> + p_sys->live = false;
> + vlc_cond_signal (&p_sys->wait);
> + vlc_mutex_unlock(&p_sys->lock);
> +
> + vlc_interrupt_kill(p_sys->interrupt);
> + vlc_join (p_sys->thread , NULL);
> + vlc_interrupt_destroy(p_sys->interrupt);
> +
> + vlc_player_Lock (p_sys->player);
> + vlc_player_RemoveListener (p_sys->player , p_sys->player_listener);
> + vlc_player_Unlock (p_sys->player);
> +
> + vlc_player_RemoveTimer (p_sys->player , p_sys->timer_listener);
> +
> + DeleteSongQueue(p_intf);
> + DeleteSong(&p_sys->p_current_song);
> +
> + vlc_UrlClean (&p_sys->p_submit_url);
> + free(p_sys->psz_user_token);
> +
> + free (p_sys);
> +}
> +
> +static void *Run (void *data)
> +{
> + intf_thread_t *p_intf = data;
> + bool b_wait = 0;
> + char *request , *payload;
> +
> + intf_sys_t *p_sys = p_intf->p_sys;
> +
> + vlc_interrupt_set(p_sys->interrupt);
> +
> + vlc_mutex_lock (&p_sys->lock);
> + for (;;)
> + {
> + if (b_wait)
> + {
> + vlc_tick_t deadline = vlc_tick_now() + VLC_TICK_FROM_SEC
> (60);
> + int ret = 0;
> + while (p_sys->live && ret == 0) // wait for 1 min
> + ret = vlc_cond_timedwait (&p_sys->wait , &p_sys->lock,
> deadline);
> + }
> +
> + while (p_sys->live && p_sys->queue.size == 0)
> + vlc_cond_wait (&p_sys->wait , &p_sys->lock);
> +
> + if (!p_sys->live)
> + break;
> +
> + payload = PreparePayload (p_intf);
> + vlc_mutex_unlock (&p_sys->lock);
> +
> + if (!payload)
> + {
> + msg_Warn (p_intf , "Error: Unable to generate payload");
> + return NULL;
> + }
> +
> + request = PrepareRequest (p_intf , payload);
> + if (!request)
> + {
> + msg_Warn (p_intf , "Error: Unable to generate request
> body");
> + return NULL;
> + }
> +
> + int ret = SendRequest (p_intf , request);
> + free(request);
> +
> + vlc_mutex_lock (&p_sys->lock);
> +
> + if (ret == VLC_SUCCESS)
> + {
> + DeleteSongQueue(p_intf);
> + b_wait = false;
> + }
> + else
> + {
> + msg_Warn (p_intf , "Error: Could not transmit request");
> + b_wait = true;
> + }
> + }
> +
> + vlc_mutex_unlock (&p_sys->lock);
> + return NULL;
> +}
> +
> --
> 2.20.1
What about the meta_changed event ? I really think it is needed. Indeed, a track meta can be updated some time after it's opened.
>
> _______________________________________________
> 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