/*****************************************************************************
 * dbus.c : D-Bus control interface
 *****************************************************************************
 * Copyright © 2006-2008 Rafaël Carré
 * Copyright © 2007-2012 Mirsal Ennaime
 * Copyright © 2009-2012 The VideoLAN team
 * Copyright © 2013      Alex Merry
 *
 * Authors:    Rafaël Carré <funman at videolanorg>
 *             Mirsal Ennaime <mirsal at mirsal fr>
 *             Alex Merry <dev at randomguy3 me uk>
 *
 * 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., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
 *****************************************************************************/

/*
 * D-Bus Specification:
 *      http://dbus.freedesktop.org/doc/dbus-specification.html
 * D-Bus low-level C API (libdbus)
 *      http://dbus.freedesktop.org/doc/dbus/api/html/index.html
 *  extract:
 *   "If you use this low-level API directly, you're signing up for some pain."
 *
 * MPRIS Specification version 1.0
 *      http://wiki.xmms2.xmms.se/index.php/MPRIS
 */

/*****************************************************************************
 * Preamble
 *****************************************************************************/

#ifdef HAVE_CONFIG_H
# include "config.h"
#endif

#include <dbus/dbus.h>
#include "dbus_common.h"
#include "dbus_root.h"
#include "dbus_player.h"
#include "dbus_tracklist.h"
#include "dbus_introspect.h"

#define VLC_MODULE_LICENSE VLC_LICENSE_GPL_2_PLUS
#include <vlc_common.h>
#include <vlc_arrays.h>
#include <vlc_threads.h>
#include <vlc_plugin.h>
#include <vlc_interface.h>
#include <vlc_meta.h>
#include <vlc_tick.h>
#include <vlc_fs.h>

#include <assert.h>
#include <limits.h>
#include <string.h>

#include <poll.h>
#include <errno.h>
#include <unistd.h>

#define DBUS_MPRIS_BUS_NAME "org.mpris.MediaPlayer2.vlc"
#define DBUS_INSTANCE_ID_PREFIX "instance"

#define SEEK_THRESHOLD 1000 /* µsec */
#define EVENTS_DELAY INT64_C(100000) /* 100 ms */

/*****************************************************************************
 * Local prototypes.
 *****************************************************************************/

static DBusHandlerResult
MPRISEntryPoint ( DBusConnection *p_conn, DBusMessage *p_from, void *p_this );

static const DBusObjectPathVTable dbus_mpris_vtable = {
        NULL, MPRISEntryPoint, /* handler function */
        NULL, NULL, NULL, NULL
};

typedef struct
{
    int signal;
    union {
        tracklist_append_event_t *items_appended;
        tracklist_remove_event_t *items_removed;
    };
} callback_info_t;

enum
{
    PIPE_OUT = 0,
    PIPE_IN  = 1
};

static int  Open    ( vlc_object_t * );
static void Close   ( vlc_object_t * );
static void *Run    ( void * );

static int TrackChange( intf_thread_t * );

static dbus_bool_t add_timeout(DBusTimeout *, void *);
static void remove_timeout(DBusTimeout *, void *);
static void toggle_timeout(DBusTimeout *, void *);

static dbus_bool_t add_watch   ( DBusWatch *p_watch, void *p_data );
static void remove_watch    ( DBusWatch *p_watch, void *p_data );
static void watch_toggled   ( DBusWatch *p_watch, void *p_data );

static void wakeup_main_loop( void *p_data );

static void ProcessEvents  ( intf_thread_t    *p_intf,
                             callback_info_t **p_events,
                             int               i_events );

static void ProcessWatches ( intf_thread_t    *p_intf,
                             DBusWatch       **p_watches,
                             int               i_watches,
                             struct pollfd    *p_fds,
                             int               i_fds );

static void DispatchDBusMessages( intf_thread_t *p_intf );


static void playlist_on_items_added(vlc_playlist_t *,
                                    size_t, vlc_playlist_item_t *const [],
                                    size_t, void *);
static void playlist_on_items_removed(vlc_playlist_t *,
                                      size_t, size_t, void *);
static void playlist_on_playback_repeat_changed(vlc_playlist_t *,
                                                enum vlc_playlist_playback_repeat,
                                                void *);
static void playlist_on_playback_order_changed(vlc_playlist_t *,
                                               enum vlc_playlist_playback_order,
                                               void *);
static void playlist_on_current_index_changed(vlc_playlist_t *,
                                              ssize_t, void *);
static void playlist_on_has_prev_changed(vlc_playlist_t *playlist,
                                         bool has_prev, void *data);
static void playlist_on_has_next_changed(vlc_playlist_t *playlist,
                                         bool has_next, void *data);

static void player_on_state_changed(vlc_player_t *,
                                    enum vlc_player_state, void *);
static void player_on_error_changed(vlc_player_t *,
                                    enum vlc_player_error, void *);
static void player_on_rate_changed(vlc_player_t *, float, void *);
static void player_on_capabilities_changed(vlc_player_t *, int, int, void *);
static void player_on_media_meta_changed(vlc_player_t *,
                                         input_item_t *, void *);

static void player_aout_on_volume_changed(audio_output_t *, float, void *);
static void player_aout_on_mute_changed(audio_output_t *, bool, void *);

static void player_vout_on_fullscreen_changed(vout_thread_t *, bool, void *);
static void player_timer_on_discontinuity(vlc_tick_t system_dae, void *data);
static void player_timer_on_update(const struct vlc_player_timer_point *, void *);

/*****************************************************************************
 * Module descriptor
 *****************************************************************************/
vlc_module_begin ()
    set_shortname( N_("DBus"))
    set_description( N_("D-Bus control interface") )
    set_capability( "interface", 0 )
    set_callbacks( Open, Close )
vlc_module_end ()

/*****************************************************************************
 * Open: initialize interface
 *****************************************************************************/

static int Open( vlc_object_t *p_this )
{
    intf_thread_t   *p_intf = (intf_thread_t*)p_this;
    vlc_object_t *vlc = VLC_OBJECT(vlc_object_instance(p_this));
    vlc_playlist_t *playlist = NULL;

    /* initialisation of the connection */
    if( !dbus_threads_init_default() )
        return VLC_EGENERIC;

    intf_sys_t *p_sys  = calloc( 1, sizeof( intf_sys_t ) );
    if( unlikely(!p_sys) )
        return VLC_ENOMEM;

    DBusConnection  *p_conn;
    p_sys->i_player_caps   = PLAYER_CAPS_NONE;
    p_sys->i_playing_state = PLAYBACK_STATE_INVALID;

    if( vlc_pipe( p_sys->p_pipe_fds ) )
    {
        free( p_sys );
        msg_Err( p_intf, "Could not create pipe" );
        return VLC_EGENERIC;
    }

    DBusError error;
    dbus_error_init( &error );

    /* connect privately to the session bus
     * the connection will not be shared with other vlc modules which use dbus,
     * thus avoiding a whole class of concurrency issues */
    p_conn = dbus_bus_get_private( DBUS_BUS_SESSION, &error );
    if( !p_conn )
    {
        msg_Err( p_this, "Failed to connect to the D-Bus session daemon: %s",
                error.message );
        dbus_error_free( &error );
        goto dbus_connection_failure;
    }

    dbus_connection_set_exit_on_disconnect( p_conn, FALSE );

    /* Register the entry point object path */
    dbus_connection_register_object_path( p_conn, DBUS_MPRIS_OBJECT_PATH,
            &dbus_mpris_vtable, p_this );

    /* Try to register org.mpris.MediaPlayer2.vlc */
    const unsigned bus_flags = DBUS_NAME_FLAG_DO_NOT_QUEUE;
    var_Create(vlc, "dbus-mpris-name", VLC_VAR_STRING);
    if( dbus_bus_request_name( p_conn, DBUS_MPRIS_BUS_NAME, bus_flags, NULL )
                                     != DBUS_REQUEST_NAME_REPLY_PRIMARY_OWNER )
    {
        /* Register an instance-specific well known name of the form
         * org.mpris.MediaPlayer2.vlc.instanceXXXX where XXXX is the
         * current Process ID */
        char unique_service[sizeof( DBUS_MPRIS_BUS_NAME ) +
                            sizeof( DBUS_INSTANCE_ID_PREFIX ) + 10];

        snprintf( unique_service, sizeof (unique_service),
                  DBUS_MPRIS_BUS_NAME"."DBUS_INSTANCE_ID_PREFIX"%"PRIu32,
                  (uint32_t)getpid() );

        if( dbus_bus_request_name( p_conn, unique_service, bus_flags, NULL )
                                     == DBUS_REQUEST_NAME_REPLY_PRIMARY_OWNER )
        {
            msg_Dbg( p_intf, "listening on dbus as: %s", unique_service );
            var_SetString(vlc, "dbus-mpris-name", unique_service);
        }
    }
    else
    {
        msg_Dbg( p_intf, "listening on dbus as: %s", DBUS_MPRIS_BUS_NAME );
        var_SetString(vlc, "dbus-mpris-name", DBUS_MPRIS_BUS_NAME);
    }
    dbus_connection_flush( p_conn );

    p_intf->p_sys = p_sys;
    p_sys->p_conn = p_conn;
    vlc_array_init( &p_sys->events );
    vlc_array_init( &p_sys->timeouts );
    vlc_array_init( &p_sys->watches );
    vlc_mutex_init( &p_sys->lock );

    p_sys->playlist = playlist = vlc_intf_GetMainPlaylist(p_intf);
    vlc_player_t *player = vlc_playlist_GetPlayer(playlist);

    vlc_playlist_Lock(playlist);

    static struct vlc_playlist_callbacks const playlist_cbs =
    {
        .on_items_added = playlist_on_items_added,
        .on_items_removed = playlist_on_items_removed,
        .on_playback_repeat_changed = playlist_on_playback_repeat_changed,
        .on_playback_order_changed = playlist_on_playback_order_changed,
        .on_current_index_changed = playlist_on_current_index_changed,
        .on_has_prev_changed = playlist_on_has_prev_changed,
        .on_has_next_changed = playlist_on_has_next_changed,
    };
    p_sys->playlist_listener =
        vlc_playlist_AddListener(playlist, &playlist_cbs, p_intf, false);
    if (!p_sys->playlist_listener)
        goto playlist_listener_failure;

    static struct vlc_player_cbs const player_cbs =
    {
        .on_state_changed = player_on_state_changed,
        .on_error_changed = player_on_error_changed,
        .on_rate_changed = player_on_rate_changed,
        .on_capabilities_changed = player_on_capabilities_changed,
        .on_media_meta_changed = player_on_media_meta_changed,
    };
    p_sys->player_listener =
        vlc_player_AddListener(player, &player_cbs, p_intf);
    if (!p_sys->player_listener)
        goto player_listener_failure;

    static struct vlc_player_aout_cbs const player_aout_cbs =
    {
        .on_volume_changed = player_aout_on_volume_changed,
        .on_mute_changed = player_aout_on_mute_changed
    };
    p_sys->player_aout_listener =
        vlc_player_aout_AddListener(player, &player_aout_cbs, p_intf);
    if (!p_sys->player_aout_listener)
        goto player_aout_listener_failure;

    static struct vlc_player_vout_cbs const player_vout_cbs =
    {
        .on_fullscreen_changed = player_vout_on_fullscreen_changed,
    };
    p_sys->player_vout_listener =
        vlc_player_vout_AddListener(player, &player_vout_cbs, p_intf);
    if (!p_sys->player_vout_listener)
        goto player_vout_listener_failure;

    static struct vlc_player_timer_cbs const player_timer_cbs =
    {
        .on_update = player_timer_on_update,
        .on_discontinuity = player_timer_on_discontinuity,
    };
    p_sys->player_timer =
        vlc_player_AddTimer(player, VLC_TICK_FROM_SEC(1), &player_timer_cbs, p_intf);
    if (!p_sys->player_timer)
        goto player_timer_failure;

    vlc_playlist_Unlock(playlist);

    if( !dbus_connection_set_timeout_functions( p_conn,
                                                add_timeout,
                                                remove_timeout,
                                                toggle_timeout,
                                                p_intf, NULL ) )
        goto late_failure;

    if( !dbus_connection_set_watch_functions( p_conn,
                                              add_watch,
                                              remove_watch,
                                              watch_toggled,
                                              p_intf, NULL ) )
        goto late_failure;

    if( vlc_clone( &p_sys->thread, Run, p_intf ) )
        goto late_failure;

    return VLC_SUCCESS;

late_failure:
    vlc_playlist_Lock(playlist);
    vlc_player_RemoveTimer(player, p_sys->player_timer);
player_timer_failure:
    vlc_player_vout_RemoveListener(player, p_sys->player_vout_listener);
player_vout_listener_failure:
    vlc_player_aout_RemoveListener(player, p_sys->player_aout_listener);
player_aout_listener_failure:
    vlc_player_RemoveListener(player, p_sys->player_listener);
player_listener_failure:
    vlc_playlist_RemoveListener(playlist, p_sys->playlist_listener);
playlist_listener_failure:
    vlc_playlist_Unlock(playlist);

    var_Destroy(vlc, "dbus-mpris-name");
    /* The dbus connection is private,
     * so we are responsible for closing it
     * XXX: Does this make sense when OOM ? */
    dbus_connection_close( p_sys->p_conn );
    dbus_connection_unref( p_conn );

dbus_connection_failure:
    vlc_close( p_sys->p_pipe_fds[1] );
    vlc_close( p_sys->p_pipe_fds[0] );

    free( p_sys );
    return VLC_EGENERIC;
}

/*****************************************************************************
 * Close: destroy interface
 *****************************************************************************/

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 );

    vlc_player_t *player = vlc_playlist_GetPlayer(playlist);
    vlc_playlist_Lock(playlist);
    vlc_player_RemoveTimer(player, p_sys->player_timer);
    vlc_player_vout_RemoveListener(player, p_sys->player_vout_listener);
    vlc_player_aout_RemoveListener(player, p_sys->player_aout_listener);
    vlc_player_RemoveListener(player, p_sys->player_listener);
    vlc_playlist_RemoveListener(playlist, p_sys->playlist_listener);
    vlc_playlist_Unlock(playlist);

    /* The dbus connection is private, so we are responsible
     * for closing it */
    dbus_connection_close( p_sys->p_conn );
    dbus_connection_unref( p_sys->p_conn );

    // Free the events array
    for( size_t i = 0; i < vlc_array_count( &p_sys->events ); i++ )
    {
        callback_info_t* info = vlc_array_item_at_index( &p_sys->events, i );
        switch( info->signal ) {
        case SIGNAL_PLAYLIST_ITEM_APPEND:
            tracklist_append_event_destroy( info->items_appended );
            break;
        case SIGNAL_PLAYLIST_ITEM_DELETED:
            tracklist_remove_event_destroy( info->items_removed );
            break;
        }
        free( info );
    }
    vlc_array_clear( &p_sys->events );
    vlc_array_clear( &p_sys->timeouts );
    vlc_array_clear( &p_sys->watches );
    vlc_close( p_sys->p_pipe_fds[1] );
    vlc_close( p_sys->p_pipe_fds[0] );
    free( p_sys );
}

static dbus_bool_t add_timeout(DBusTimeout *to, void *data)
{
    intf_thread_t *intf = data;
    intf_sys_t *sys = intf->p_sys;

    vlc_tick_t *expiry = malloc(sizeof (*expiry));
    if (unlikely(expiry == NULL))
        return FALSE;

    dbus_timeout_set_data(to, expiry, free);

    vlc_mutex_lock(&sys->lock);
    vlc_array_append_or_abort(&sys->timeouts, to);
    vlc_mutex_unlock(&sys->lock);

    return TRUE;
}

static void remove_timeout(DBusTimeout *to, void *data)
{
    intf_thread_t *intf = data;
    intf_sys_t *sys = intf->p_sys;
    size_t idx;

    vlc_mutex_lock(&sys->lock);
    idx = vlc_array_index_of_item(&sys->timeouts, to);
    vlc_array_remove(&sys->timeouts, idx);
    vlc_mutex_unlock(&sys->lock);
}

static void toggle_timeout(DBusTimeout *to, void *data)
{
    intf_thread_t *intf = data;
    intf_sys_t *sys = intf->p_sys;
    vlc_tick_t *expiry = dbus_timeout_get_data(to);

    vlc_mutex_lock(&sys->lock);
    if (dbus_timeout_get_enabled(to))
        *expiry = vlc_tick_now() + UINT64_C(1000) * dbus_timeout_get_interval(to);
    vlc_mutex_unlock(&sys->lock);

    wakeup_main_loop(intf);
}

/**
 * Computes the time until the next timeout expiration.
 * @note Interface lock must be held.
 * @return The time in milliseconds until the next expiration,
 *         or -1 if there are no pending timeouts.
 */
static int next_timeout(intf_thread_t *intf)
{
    intf_sys_t *sys = intf->p_sys;
    vlc_tick_t next_timeout = VLC_TICK_MAX;
    unsigned count = vlc_array_count(&sys->timeouts);

    for (unsigned i = 0; i < count; i++)
    {
        DBusTimeout *to = vlc_array_item_at_index(&sys->timeouts, i);

        if (!dbus_timeout_get_enabled(to))
            continue;

        vlc_tick_t *expiry = dbus_timeout_get_data(to);

        if (next_timeout > *expiry)
            next_timeout = *expiry;
    }

    if (next_timeout == VLC_TICK_MAX)
        return -1;

    if (MS_FROM_VLC_TICK(next_timeout) > INT_MAX)
        return INT_MAX;

    return MS_FROM_VLC_TICK(next_timeout);
}

/**
 * Process pending D-Bus timeouts.
 *
 * @note Interface lock must be held.
 */
static void process_timeouts(intf_thread_t *intf)
{
    intf_sys_t *sys = intf->p_sys;

    for (size_t i = 0; i < vlc_array_count(&sys->timeouts); i++)
    {
        DBusTimeout *to = vlc_array_item_at_index(&sys->timeouts, i);

        if (!dbus_timeout_get_enabled(to))
            continue;

        vlc_tick_t *expiry = dbus_timeout_get_data(to);
        if (*expiry > vlc_tick_now())
            continue;

        expiry += UINT64_C(1000) * dbus_timeout_get_interval(to);
        vlc_mutex_unlock(&sys->lock);

        dbus_timeout_handle(to);

        vlc_mutex_lock(&sys->lock);
        i = -1; /* lost track of state, restart from beginning */
    }
}


static dbus_bool_t add_watch( DBusWatch *p_watch, void *p_data )
{
    intf_thread_t *p_intf = (intf_thread_t*) p_data;
    intf_sys_t    *p_sys  = (intf_sys_t*) p_intf->p_sys;

    vlc_mutex_lock( &p_sys->lock );
    vlc_array_append_or_abort( &p_sys->watches, p_watch );
    vlc_mutex_unlock( &p_sys->lock );

    return TRUE;
}

static void remove_watch( DBusWatch *p_watch, void *p_data )
{
    intf_thread_t *p_intf = (intf_thread_t*) p_data;
    intf_sys_t    *p_sys  = (intf_sys_t*) p_intf->p_sys;
    size_t idx;

    vlc_mutex_lock( &p_sys->lock );
    idx = vlc_array_index_of_item( &p_sys->watches, p_watch );
    vlc_array_remove( &p_sys->watches, idx );
    vlc_mutex_unlock( &p_sys->lock );
}

static void watch_toggled( DBusWatch *p_watch, void *p_data )
{
    intf_thread_t *p_intf = (intf_thread_t*) p_data;

    if( dbus_watch_get_enabled( p_watch ) )
        wakeup_main_loop( p_intf );
}

/**
 * GetPollFds() fills an array of pollfd data structures with :
 *  - the set of enabled dbus watches
 *  - the unix pipe which we use to manually wake up the main loop
 *
 * This function must be called with p_sys->lock locked
 *
 * @return The number of file descriptors
 *
 * @param intf_thread_t *p_intf this interface thread's state
 * @param struct pollfd *p_fds a pointer to a pollfd array large enough to
 * contain all the returned data (number of enabled dbus watches + 1)
 */
static int GetPollFds( intf_thread_t *p_intf, struct pollfd *p_fds )
{
    intf_sys_t *p_sys = p_intf->p_sys;
    size_t i_watches = vlc_array_count( &p_sys->watches );
    int i_fds = 1;

    p_fds[0].fd = p_sys->p_pipe_fds[PIPE_OUT];
    p_fds[0].events = POLLIN | POLLPRI;

    for( size_t i = 0; i < i_watches; i++ )
    {
        DBusWatch *p_watch = NULL;
        p_watch = vlc_array_item_at_index( &p_sys->watches, i );
        if( !dbus_watch_get_enabled( p_watch ) )
            continue;

        p_fds[i_fds].fd = dbus_watch_get_unix_fd( p_watch );
        int i_flags = dbus_watch_get_flags( p_watch );

        if( i_flags & DBUS_WATCH_READABLE )
            p_fds[i_fds].events |= POLLIN | POLLPRI;

        if( i_flags & DBUS_WATCH_WRITABLE )
            p_fds[i_fds].events |= POLLOUT;

        i_fds++;
    }

    return i_fds;
}


/**
 * ProcessPlaylistChanged() reacts to tracks being either inserted or removed from the playlist
 *
 * This function must be called by ProcessEvents only
 *
 * @param intf_thread_t *p_intf This interface thread state
 * @param callback_info_t *p_events the list of events to process
 */
static void ProcessPlaylistChanged( intf_thread_t *p_intf,
                                    vlc_dictionary_t *player_properties,
                                    vlc_dictionary_t *tracklist_properties )
{
    vlc_playlist_t *playlist = p_intf->p_sys->playlist;
    vlc_playlist_Lock(playlist);
    bool b_can_play = vlc_playlist_Count(playlist) > 0;
    vlc_playlist_Unlock(playlist);

    if( b_can_play != p_intf->p_sys->b_can_play )
    {
        p_intf->p_sys->b_can_play = b_can_play;
        vlc_dictionary_insert( player_properties, "CanPlay", NULL );
    }

    if( !vlc_dictionary_has_key( tracklist_properties, "Tracks" ) )
        vlc_dictionary_insert( tracklist_properties, "Tracks", NULL );
}

/**
 * ProcessEvents() reacts to a list of events originating from other VLC threads
 *
 * This function must be called with p_sys->lock unlocked
 *
 * @param intf_thread_t *p_intf This interface thread state
 * @param callback_info_t *p_events the list of events to process
 */
static void ProcessEvents( intf_thread_t *p_intf,
                           callback_info_t **p_events, int i_events )
{
    vlc_dictionary_t player_properties, tracklist_properties, root_properties;
    vlc_dictionary_init( &player_properties,    0 );
    vlc_dictionary_init( &tracklist_properties, 0 );
    vlc_dictionary_init( &root_properties,      0 );

    // In case multiple *_ITEM_APPEND or *_ITEM_DELETED events appear on the
    // list, the elements in their respective map values will be linked in
    // order.
    // We keep the tail of the list in order to append the elements to the end
    // of each list.
    tracklist_append_event_t *last_append = NULL;
    tracklist_remove_event_t *last_remove = NULL;

    for( int i = 0; i < i_events; i++ )
    {
        switch( p_events[i]->signal )
        {
        case SIGNAL_ITEM_CURRENT:
            TrackChange( p_intf );

            // rate depends on current item
            if( !vlc_dictionary_has_key( &player_properties, "Rate" ) )
                vlc_dictionary_insert( &player_properties, "Rate", NULL );

            vlc_dictionary_insert( &player_properties, "Metadata", NULL );
            break;
        case SIGNAL_PLAYLIST_ITEM_APPEND:
            if ( !last_append ) {
                last_append = vlc_dictionary_value_for_key( &tracklist_properties, "TrackAdded" );
            }
            if( !last_append ) {
                vlc_dictionary_insert( &tracklist_properties, "TrackAdded", p_events[i]->items_appended );

                last_append = p_events[i]->items_appended;
            } else {
                last_append->change_ev.next = &p_events[i]->items_appended->change_ev;
                last_append = p_events[i]->items_appended;
            }
            ProcessPlaylistChanged( p_intf, &player_properties, &tracklist_properties );
            break;
        case SIGNAL_PLAYLIST_ITEM_DELETED:
            if( !last_remove ) {
                assert (!vlc_dictionary_has_key( &tracklist_properties, "TrackRemoved" ) );
                vlc_dictionary_insert( &tracklist_properties, "TrackRemoved", p_events[i]->items_removed );

                last_remove = p_events[i]->items_removed;
            } else {
                last_remove->change_ev.next = &p_events[i]->items_removed->change_ev;
                last_remove = p_events[i]->items_removed;
            }
            ProcessPlaylistChanged( p_intf, &player_properties, &tracklist_properties );
            break;
        case SIGNAL_VOLUME_MUTED:
        case SIGNAL_VOLUME_CHANGE:
            vlc_dictionary_insert( &player_properties, "Volume", NULL );
            break;
        case SIGNAL_RANDOM:
            vlc_dictionary_insert( &player_properties, "Shuffle", NULL );
            break;
        case SIGNAL_FULLSCREEN:
            vlc_dictionary_insert( &root_properties, "Fullscreen", NULL );
            break;
        case SIGNAL_REPEAT:
        case SIGNAL_LOOP:
            vlc_dictionary_insert( &player_properties, "LoopStatus", NULL );
            break;
        case SIGNAL_STATE:
            vlc_dictionary_insert( &player_properties, "PlaybackStatus", NULL );
            break;
        case SIGNAL_RATE:
            vlc_dictionary_insert( &player_properties, "Rate", NULL );
            break;
        case SIGNAL_INPUT_METADATA:
        {
            vlc_player_t *player =
                vlc_playlist_GetPlayer(p_intf->p_sys->playlist);
            vlc_player_Lock(player);
            input_item_t *p_item = vlc_player_GetCurrentMedia(player);
            if( p_item )
                vlc_dictionary_insert( &player_properties,
                                       "Metadata", NULL );
            vlc_player_Unlock(player);
            break;
        }
        case SIGNAL_CAN_SEEK:
            vlc_dictionary_insert( &player_properties, "CanSeek", NULL );
            break;
        case SIGNAL_CAN_PAUSE:
            vlc_dictionary_insert( &player_properties, "CanPause", NULL );
            break;
        case SIGNAL_SEEK:
            SeekedEmit( p_intf );
            break;
        case SIGNAL_CAN_GO_PREVIOUS:
            vlc_dictionary_insert( &player_properties, "CanGoPrevious", NULL );
            break;
        case SIGNAL_CAN_GO_NEXT:
            vlc_dictionary_insert( &player_properties, "CanGoNext", NULL );
            break;
        default:
            vlc_assert_unreachable();
        }
    }

    if( !vlc_dictionary_is_empty( &player_properties ) )
        PlayerPropertiesChangedEmit( p_intf, &player_properties );

    if( !vlc_dictionary_is_empty( &tracklist_properties ) )
        TrackListPropertiesChangedEmit( p_intf, &tracklist_properties );

    if( !vlc_dictionary_is_empty( &root_properties ) )
        RootPropertiesChangedEmit( p_intf, &root_properties );


    for (int i = 0; i < i_events; i++)
    {
        switch (p_events[i]->signal)
        {
        case SIGNAL_PLAYLIST_ITEM_APPEND:
            tracklist_append_event_destroy(p_events[i]->items_appended);
            break;
        case SIGNAL_PLAYLIST_ITEM_DELETED:
            tracklist_remove_event_destroy(p_events[i]->items_removed);
            break;
        default:
            break;
        }
        free(p_events[i]);
    }

    vlc_dictionary_clear( &player_properties,    NULL, NULL );
    vlc_dictionary_clear( &tracklist_properties, NULL, NULL );
    vlc_dictionary_clear( &root_properties,      NULL, NULL );
}

/**
 * ProcessWatches() handles a list of dbus watches after poll() has returned
 *
 * This function must be called with p_sys->lock unlocked
 *
 * @param intf_thread_t *p_intf This interface thread state
 * @param DBusWatch **p_watches The list of dbus watches to process
 * @param int i_watches The size of the p_watches array
 * @param struct pollfd *p_fds The result of a poll() call
 * @param int i_fds The number of file descriptors processed by poll()
 */
static void ProcessWatches( intf_thread_t *p_intf,
                            DBusWatch **p_watches, int i_watches,
                            struct pollfd *p_fds,  int i_fds )
{
    VLC_UNUSED(p_intf);

    /* Process watches */
    for( int i = 0; i < i_watches; i++ )
    {
        DBusWatch *p_watch = p_watches[i];
        if( !dbus_watch_get_enabled( p_watch ) )
            continue;

        for( int j = 0; j < i_fds; j++ )
        {
            if( p_fds[j].fd != dbus_watch_get_unix_fd( p_watch ) )
                continue;

            int i_flags   = 0;
            int i_revents = p_fds[j].revents;

            if( i_revents & POLLIN )
                i_flags |= DBUS_WATCH_READABLE;

            if( i_revents & POLLOUT )
                i_flags |= DBUS_WATCH_WRITABLE;

            if( i_revents & POLLERR )
                i_flags |= DBUS_WATCH_ERROR;

            if( i_revents & POLLHUP )
                i_flags |= DBUS_WATCH_HANGUP;

            if( i_flags )
                dbus_watch_handle( p_watch, i_flags );
        }
    }
}

/**
 * DispatchDBusMessages() dispatches incoming dbus messages
 * (indirectly invoking the callbacks), then it sends outgoing
 * messages which needs to be sent on the bus (method replies and signals)
 *
 * This function must be called with p_sys->lock unlocked
 *
 * @param intf_thread_t *p_intf This interface thread state
 */
static void DispatchDBusMessages( intf_thread_t *p_intf )
{
    DBusDispatchStatus status;
    intf_sys_t *p_sys = p_intf->p_sys;

    /* Dispatch incoming messages */
    status = dbus_connection_get_dispatch_status( p_sys->p_conn );
    while( status != DBUS_DISPATCH_COMPLETE )
    {
        dbus_connection_dispatch( p_sys->p_conn );
        status = dbus_connection_get_dispatch_status( p_sys->p_conn );
    }

    /* Send outgoing data */
    if( dbus_connection_has_messages_to_send( p_sys->p_conn ) )
        dbus_connection_flush( p_sys->p_conn );
}

/**
 * MPRISEntryPoint() routes incoming messages to their respective interface
 * implementation.
 *
 * This function is called during dbus_connection_dispatch()
 */
static DBusHandlerResult
MPRISEntryPoint ( DBusConnection *p_conn, DBusMessage *p_from, void *p_this )
{
    const char *psz_target_interface;
    const char *psz_interface = dbus_message_get_interface( p_from );
    const char *psz_method    = dbus_message_get_member( p_from );

    DBusError error;

    if( psz_interface && strcmp( psz_interface, DBUS_INTERFACE_PROPERTIES ) )
        psz_target_interface = psz_interface;

    else
    {
        dbus_error_init( &error );
        dbus_message_get_args( p_from, &error,
                               DBUS_TYPE_STRING, &psz_target_interface,
                               DBUS_TYPE_INVALID );

        if( dbus_error_is_set( &error ) )
        {
            msg_Err( (vlc_object_t*) p_this, "D-Bus error on %s.%s: %s",
                                             psz_interface, psz_method,
                                             error.message );
            dbus_error_free( &error );
            return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
        }
    }

    if( !strcmp( psz_target_interface, DBUS_INTERFACE_INTROSPECTABLE ) )
        return handle_introspect( p_conn, p_from, p_this );

    if( !strcmp( psz_target_interface, DBUS_MPRIS_ROOT_INTERFACE ) )
        return handle_root( p_conn, p_from, p_this );

    if( !strcmp( psz_target_interface, DBUS_MPRIS_PLAYER_INTERFACE ) )
        return handle_player( p_conn, p_from, p_this );

    if( !strcmp( psz_target_interface, DBUS_MPRIS_TRACKLIST_INTERFACE ) )
        return handle_tracklist( p_conn, p_from, p_this );

    return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
}

/*****************************************************************************
 * Run: main loop
 *****************************************************************************/

static void *Run( void *data )
{
    intf_thread_t *p_intf = data;
    intf_sys_t    *p_sys = p_intf->p_sys;

    vlc_thread_set_name("vlc-ctrl-dbus");

    int canc = vlc_savecancel();

    vlc_tick_t events_last_date = VLC_TICK_INVALID;
    int events_poll_timeout = -1;
    for( ;; )
    {
        vlc_mutex_lock( &p_sys->lock );

        size_t i_watches = vlc_array_count( &p_sys->watches );
        struct pollfd fds[i_watches];
        memset(fds, 0, sizeof fds);

        int i_fds = GetPollFds( p_intf, fds );
        int timeout = next_timeout(p_intf);

        vlc_mutex_unlock( &p_sys->lock );

        /* thread cancellation is allowed while the main loop sleeps */
        vlc_restorecancel( canc );
        if( timeout == -1 )
            timeout = events_poll_timeout;

        while (poll(fds, i_fds, timeout) == -1)
        {
            if (errno != EINTR)
                goto error;
        }

        canc = vlc_savecancel();

        /* Was the main loop woken up manually ? */
        if (fds[0].revents & POLLIN)
        {
            while (read(fds[0].fd, &(char){' '}, 1) == -1)
            {
                /* Only EINTR can happen here, so ignore the error. */
            }
        }

        /* We need to lock the mutex while building lists of events,
         * timeouts and watches to process but we can't keep the lock while
         * processing them, or else we risk a deadlock:
         *
         * The signal functions could lock mutex X while p_events is locked;
         * While some other function in vlc (playlist) might lock mutex X
         * and then set a variable which would call PlaylistCallback(), which itself
         * needs to lock p_events to add a new event.
         */
        vlc_mutex_lock( &p_intf->p_sys->lock );

        process_timeouts(p_intf);

        /* Get the list of watches to process */
        i_watches = vlc_array_count( &p_sys->watches );
        DBusWatch *p_watches[i_watches ? i_watches : 1];
        for( size_t i = 0; i < i_watches; i++ )
        {
            p_watches[i] = vlc_array_item_at_index( &p_sys->watches, i );
        }

        /* Get the list of events to process */
        size_t i_events = vlc_array_count( &p_sys->events );
        callback_info_t** pp_info = NULL;

        if( i_events > 0 )
        {
            vlc_tick_t now = vlc_tick_now();
            if( events_last_date == VLC_TICK_INVALID
             || now - events_last_date > EVENTS_DELAY )
            {
                /* Send events every EVENTS_DELAY */
                events_last_date = now;
                events_poll_timeout = -1;

                pp_info = vlc_alloc( i_events, sizeof(*pp_info) );
                if( pp_info )
                {
                    for( size_t i = 0; i < i_events; i++ )
                        pp_info[i] = vlc_array_item_at_index( &p_sys->events, i );
                    vlc_array_clear( &p_sys->events );
                }
            }
            else if( events_poll_timeout == -1 )
            {
                /* Request poll to wake up in order to send these events after
                 * some delay */
                events_poll_timeout = ( EVENTS_DELAY - ( now - events_last_date ) ) / 1000;
            }
        }
        else /* No events: clear timeout */
            events_poll_timeout = -1;

        /* now we can release the lock and process what's pending */
        vlc_mutex_unlock( &p_intf->p_sys->lock );

        if( pp_info )
        {
            ProcessEvents( p_intf, pp_info, i_events );
            free( pp_info );
        }
        ProcessWatches( p_intf, p_watches, i_watches, fds, i_fds );

        DispatchDBusMessages( p_intf );
    }
error:
    vlc_restorecancel(canc);
    return NULL;
}

static void   wakeup_main_loop( void *p_data )
{
    intf_thread_t *p_intf = (intf_thread_t*) p_data;

    if( !write( p_intf->p_sys->p_pipe_fds[PIPE_IN], "\0", 1 ) )
        msg_Err( p_intf, "Could not wake up the main loop: %s",
                 vlc_strerror_c(errno) );
}

static bool add_event_locked( intf_thread_t *p_intf, const callback_info_t *p_info )
{
    if( !p_info->signal )
        return false;

    for( size_t i = 0; i < vlc_array_count( &p_intf->p_sys->events ); ++ i )
    {
        callback_info_t *oldinfo =
            vlc_array_item_at_index( &p_intf->p_sys->events, i );

        switch (p_info->signal)
        {
            /* Those events are squashed afterwards and must be
             * released appropriately. */
            case SIGNAL_PLAYLIST_ITEM_APPEND:
            case SIGNAL_PLAYLIST_ITEM_DELETED:
                break;
            default:
                if (p_info->signal == oldinfo->signal)
                    return false;
        }
    }

    callback_info_t *p_dup = malloc( sizeof( *p_dup ) );
    if( unlikely(p_dup == NULL) )
        return false;
    *p_dup = *p_info;

    vlc_array_append( &p_intf->p_sys->events, p_dup );
    return true;
}

static bool add_event_signal( intf_thread_t *p_intf, const callback_info_t *p_info )
{
    intf_sys_t *p_sys = p_intf->p_sys;
    vlc_mutex_lock(&p_sys->lock);
    bool added = add_event_locked(p_intf, p_info);
    vlc_mutex_unlock(&p_sys->lock);

    if( added )
        wakeup_main_loop( p_intf );
    return added;
}

static void
playlist_on_items_added(vlc_playlist_t *playlist, size_t index,
                        vlc_playlist_item_t *const items[], size_t count,
                        void *data)
{
    tracklist_append_event_t *append_event = tracklist_append_event_create(index, items, count);
    bool added = add_event_signal(data,
            &(callback_info_t){ .signal = SIGNAL_PLAYLIST_ITEM_APPEND,
                                .items_appended = append_event });
    if (!added)
        tracklist_append_event_destroy(append_event);
    (void) playlist;
}

static void
playlist_on_items_removed(vlc_playlist_t *playlist,
                          size_t index, size_t count, void *data)
{
    tracklist_remove_event_t *remove_event = tracklist_remove_event_create(index, count);
    bool added = add_event_signal(data,
            &(callback_info_t){ .signal = SIGNAL_PLAYLIST_ITEM_DELETED,
                                .items_removed = remove_event });
    if (!added)
        tracklist_remove_event_destroy(remove_event);
    (void) playlist;
}

static void
playlist_on_playback_repeat_changed(vlc_playlist_t *playlist,
                                    enum vlc_playlist_playback_repeat repeat,
                                    void *data)
{
    add_event_signal(data, &(callback_info_t){ .signal = SIGNAL_REPEAT });
    (void) playlist; (void) repeat;
}

static void
playlist_on_playback_order_changed(vlc_playlist_t *playlist,
                                   enum vlc_playlist_playback_order order,
                                   void *data)
{
    add_event_signal(data, &(callback_info_t){ .signal = SIGNAL_RANDOM });
    (void) playlist; (void) order;
}

static void
playlist_on_current_index_changed(vlc_playlist_t *playlist,
                                  ssize_t index, void *data)
{
    add_event_signal(data,
            &(callback_info_t){ .signal = SIGNAL_ITEM_CURRENT });
    (void) playlist; (void) index;
}

static void
playlist_on_has_prev_changed(vlc_playlist_t *playlist, bool has_prev,
                             void *data)
{
    add_event_signal(data,
            &(callback_info_t){ .signal = SIGNAL_CAN_GO_PREVIOUS });
    (void) playlist; (void) has_prev;
}

static void
playlist_on_has_next_changed(vlc_playlist_t *playlist, bool has_next,
                             void *data)
{
    add_event_signal(data,
            &(callback_info_t){ .signal = SIGNAL_CAN_GO_NEXT });
    (void) playlist; (void) has_next;
}

static void
player_on_state_changed(vlc_player_t *player, enum vlc_player_state state,
                        void *data)
{
    intf_thread_t *intf = data;
    intf_sys_t *sys = intf->p_sys;
    dbus_int32_t playing_state;

    switch (state)
    {
        case VLC_PLAYER_STATE_STARTED:
        case VLC_PLAYER_STATE_PLAYING:
            playing_state = PLAYBACK_STATE_PLAYING;
            break;
        case VLC_PLAYER_STATE_PAUSED:
            playing_state = PLAYBACK_STATE_PAUSED;
            break;
        /* STOPPING is used to detect time discontinuities that are coming
         * from stopping the playback in the vlc player timer callbacks. */
        case VLC_PLAYER_STATE_STOPPING:
        case VLC_PLAYER_STATE_STOPPED:
        default:
            playing_state = PLAYBACK_STATE_STOPPED;
            break;
    }

    bool added = false;
    vlc_mutex_lock(&sys->lock);
    if (playing_state != sys->i_playing_state)
    {
        sys->i_playing_state = playing_state;
        added = add_event_locked(intf,
                &(callback_info_t) { .signal = SIGNAL_STATE });
    }
    vlc_mutex_unlock(&sys->lock);

    if (added)
        wakeup_main_loop(intf);
    (void) player;
}

static void
player_on_error_changed(vlc_player_t *player, enum vlc_player_error error,
                        void *data)
{
    if (error == VLC_PLAYER_ERROR_GENERIC)
        player_on_state_changed(player, VLC_PLAYER_STATE_STOPPED, data);
}

static void
player_on_rate_changed(vlc_player_t *player, float new_rate, void *data)
{
    add_event_signal(data, &(callback_info_t){ .signal = SIGNAL_RATE });
    (void) player; (void) new_rate;
}

static void
player_on_capabilities_changed(vlc_player_t *player, int old_caps, int new_caps,
                               void *data)
{
    intf_thread_t *intf = data;
    intf_sys_t *sys = intf->p_sys;

    vlc_mutex_lock(&sys->lock);
    bool ok1 = add_event_locked(intf,
            &(callback_info_t) { .signal =  SIGNAL_CAN_SEEK });
    bool ok2 = add_event_locked(intf,
            &(callback_info_t) { .signal =  SIGNAL_CAN_PAUSE });
    vlc_mutex_unlock(&sys->lock);
    if (ok1 || ok2)
        wakeup_main_loop(intf);
    (void) player; (void) old_caps; (void) new_caps;
}

static void
player_on_media_meta_changed(vlc_player_t *player,
                             input_item_t *item, void *data)
{
    add_event_signal(data,
            &(callback_info_t){ .signal = SIGNAL_INPUT_METADATA });
    (void) player; (void) item;
}

static void
player_aout_on_volume_changed(audio_output_t *aout, float volume, void *data)
{
    add_event_signal(data,
            &(callback_info_t){ .signal = SIGNAL_VOLUME_CHANGE });
    (void) aout; (void) volume;
}

static void
player_aout_on_mute_changed(audio_output_t *aout, bool muted, void *data)
{
    add_event_signal(data,
            &(callback_info_t){ .signal = SIGNAL_VOLUME_MUTED });
    (void) aout; (void) muted;
}

static void
player_vout_on_fullscreen_changed(vout_thread_t *vout, bool enabled,
                                  void *data)
{
    add_event_signal(data, &(callback_info_t){ .signal = SIGNAL_FULLSCREEN });
    (void) vout; (void) enabled;
}

static void
player_timer_on_update(const struct vlc_player_timer_point *value, void *data)
{
    /* The callback is not used by mandatory in the vlc_player_timer API */
    VLC_UNUSED(value);
    VLC_UNUSED(data);
}

static void
player_timer_on_discontinuity(vlc_tick_t system_date, void *data)
{
    intf_thread_t *intf = data;
    intf_sys_t *sys = intf->p_sys;

    bool stopping = sys->i_playing_state == PLAYBACK_STATE_STOPPED;
    bool paused = system_date != VLC_TICK_INVALID;

    if( !paused && !stopping )
        add_event_signal(intf, &(callback_info_t){ .signal = SIGNAL_SEEK });
}

/*****************************************************************************
 * TrackChange: callback on playlist_on_current_index_changed
 *****************************************************************************/
static int TrackChange( intf_thread_t *p_intf )
{
    intf_sys_t          *p_sys      = p_intf->p_sys;

    if( p_intf->p_sys->b_dead )
        return VLC_SUCCESS;

    p_sys->has_input = false;

    p_sys->b_meta_read = false;

    vlc_player_t *player = vlc_playlist_GetPlayer(p_sys->playlist);
    vlc_player_Lock(player);
    input_item_t *item = vlc_player_GetCurrentMedia(player);
    vlc_player_Unlock(player);
    if( !item )
        return VLC_EGENERIC;

    if( input_item_IsPreparsed( item ) )
        p_sys->b_meta_read = true;

    p_sys->has_input = true;

    return VLC_SUCCESS;
}

/**
 * DemarshalSetPropertyValue() extracts the new property value from a
 * org.freedesktop.DBus.Properties.Set method call message.
 *
 * @return int VLC_SUCCESS on success
 * @param DBusMessage *p_msg a org.freedesktop.DBus.Properties.Set method call
 * @param void *p_arg placeholder for the demarshalled value
 */
int DemarshalSetPropertyValue( DBusMessage *p_msg, void *p_arg )
{
    int  i_type;
    bool b_valid_input = FALSE;
    DBusMessageIter in_args, variant;
    dbus_message_iter_init( p_msg, &in_args );

    do
    {
        i_type = dbus_message_iter_get_arg_type( &in_args );
        if( DBUS_TYPE_VARIANT == i_type )
        {
            dbus_message_iter_recurse( &in_args, &variant );
            dbus_message_iter_get_basic( &variant, p_arg );
            b_valid_input = TRUE;
        }
    } while( dbus_message_iter_next( &in_args ) );

    return b_valid_input ? VLC_SUCCESS : VLC_EGENERIC;
}

/*****************************************************************************
 * GetInputMeta: Fill a DBusMessage with the given input item metadata
 *****************************************************************************/

#define ADD_META( entry, type, data ) \
    if( data ) { \
        dbus_message_iter_open_container( &dict, DBUS_TYPE_DICT_ENTRY, \
                NULL, &dict_entry ); \
        dbus_message_iter_append_basic( &dict_entry, DBUS_TYPE_STRING, \
                &ppsz_meta_items[entry] ); \
        dbus_message_iter_open_container( &dict_entry, DBUS_TYPE_VARIANT, \
                type##_AS_STRING, &variant ); \
        dbus_message_iter_append_basic( &variant, \
                type, \
                & data ); \
        dbus_message_iter_close_container( &dict_entry, &variant ); \
        dbus_message_iter_close_container( &dict, &dict_entry ); }

#define ADD_VLC_META_STRING( entry, item ) \
    { \
        char * psz = input_item_Get##item( p_input );\
        ADD_META( entry, DBUS_TYPE_STRING, \
                  psz ); \
        free( psz ); \
    }

#define ADD_META_SINGLETON_STRING_LIST( entry, item ) \
    { \
        char * psz = input_item_Get##item( p_input );\
        if( psz ) { \
            dbus_message_iter_open_container( &dict, DBUS_TYPE_DICT_ENTRY, \
                    NULL, &dict_entry ); \
            dbus_message_iter_append_basic( &dict_entry, DBUS_TYPE_STRING, \
                    &ppsz_meta_items[entry] ); \
            dbus_message_iter_open_container( &dict_entry, DBUS_TYPE_VARIANT, \
                    "as", &variant ); \
            dbus_message_iter_open_container( &variant, DBUS_TYPE_ARRAY, "s", \
                                              &list ); \
            dbus_message_iter_append_basic( &list, \
                    DBUS_TYPE_STRING, \
                    &psz ); \
            dbus_message_iter_close_container( &variant, &list ); \
            dbus_message_iter_close_container( &dict_entry, &variant ); \
            dbus_message_iter_close_container( &dict, &dict_entry ); \
        } \
        free( psz ); \
    }

int GetInputMeta(size_t index, vlc_playlist_item_t *item, DBusMessageIter *args)
{
    input_item_t *p_input = vlc_playlist_item_GetMedia(item);
    DBusMessageIter dict, dict_entry, variant, list;
    /** The duration of the track can be expressed in second, milli-seconds and
        µ-seconds */
    dbus_int64_t i_mtime = input_item_GetDuration( p_input );
    dbus_uint32_t i_time = i_mtime / 1000000;
    dbus_int64_t i_length = i_mtime / 1000;
    char *psz_trackid;

    if (asprintf(&psz_trackid, MPRIS_TRACKID_FORMAT, index) == -1)
        return VLC_ENOMEM;

    const char* ppsz_meta_items[] =
    {
        "mpris:trackid", "xesam:url", "xesam:title", "xesam:artist",
        "xesam:album", "xesam:tracknumber", "vlc:time", "mpris:length",
        "xesam:genre", "xesam:userRating", "xesam:contentCreated",
        "mpris:artUrl", "mb:trackId", "vlc:audio-bitrate",
        "vlc:audio-samplerate", "vlc:video-bitrate", "vlc:audio-codec",
        "vlc:copyright", "xesam:comment", "vlc:encodedby", "language",
        "vlc:length", "vlc:nowplaying", "vlc:publisher", "vlc:setting",
        "status", "vlc:url", "vlc:video-codec"
    };

    dbus_message_iter_open_container( args, DBUS_TYPE_ARRAY, "{sv}", &dict );

    ADD_META( 0, DBUS_TYPE_OBJECT_PATH, psz_trackid );
    ADD_VLC_META_STRING( 1,  URI );
    ADD_VLC_META_STRING( 2,  Title );
    ADD_META_SINGLETON_STRING_LIST( 3,  Artist );
    ADD_VLC_META_STRING( 4,  Album );
    ADD_VLC_META_STRING( 5,  TrackNum );
    ADD_META( 6, DBUS_TYPE_UINT32, i_time );
    ADD_META( 7, DBUS_TYPE_INT64,  i_mtime );
    ADD_META_SINGLETON_STRING_LIST( 8,  Genre );
    //ADD_META( 9, DBUS_TYPE_DOUBLE, rating );
    ADD_VLC_META_STRING( 10, Date ); // this is supposed to be in ISO 8601 extended format
    ADD_VLC_META_STRING( 11, ArtURL );
    ADD_VLC_META_STRING( 12, TrackID );

    ADD_VLC_META_STRING( 17, Copyright );
    ADD_META_SINGLETON_STRING_LIST( 18, Description );
    ADD_VLC_META_STRING( 19, EncodedBy );
    ADD_VLC_META_STRING( 20, Language );
    ADD_META( 21, DBUS_TYPE_INT64, i_length );
    ADD_VLC_META_STRING( 22, NowPlaying );
    ADD_VLC_META_STRING( 23, Publisher );
    ADD_VLC_META_STRING( 24, Setting );
    ADD_VLC_META_STRING( 25, URL );

    free( psz_trackid );

    vlc_mutex_lock( &p_input->lock );
    if( p_input->p_meta )
    {
        int i_status = vlc_meta_GetStatus( p_input->p_meta );
        ADD_META( 23, DBUS_TYPE_INT32, i_status );
    }
    vlc_mutex_unlock( &p_input->lock );

    dbus_message_iter_close_container( args, &dict );
    return VLC_SUCCESS;
}

int AddProperty( intf_thread_t *p_intf,
                 DBusMessageIter *p_container,
                 const char* psz_property_name,
                 const char* psz_signature,
                 int (*pf_marshaller) (intf_thread_t*, DBusMessageIter*) )
{
    DBusMessageIter entry, v;

    if( !dbus_message_iter_open_container( p_container,
                                           DBUS_TYPE_DICT_ENTRY, NULL,
                                           &entry ) )
        return VLC_ENOMEM;

    if( !dbus_message_iter_append_basic( &entry,
                                         DBUS_TYPE_STRING,
                                         &psz_property_name ) )
        return VLC_ENOMEM;

    if( !dbus_message_iter_open_container( &entry,
                                           DBUS_TYPE_VARIANT, psz_signature,
                                           &v ) )
        return VLC_ENOMEM;

    if( VLC_SUCCESS != pf_marshaller( p_intf, &v ) )
        return VLC_ENOMEM;

    if( !dbus_message_iter_close_container( &entry, &v) )
        return VLC_ENOMEM;

    if( !dbus_message_iter_close_container( p_container, &entry ) )
        return VLC_ENOMEM;

    return VLC_SUCCESS;
}

#undef ADD_META
#undef ADD_VLC_META_STRING
