/*
* $Id$
*
* (c) 2004 by Kalle Wallin <kaw@linux.se>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*
*/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <time.h>
#include <string.h>
#include <glib.h>
#include "config.h"
#include "ncmpc.h"
#include "support.h"
#include "mpdclient.h"
#include "options.h"
#undef ENABLE_FANCY_PLAYLIST_MANAGMENT_CMD_ADD /* broken with song id's */
#define ENABLE_FANCY_PLAYLIST_MANAGMENT_CMD_DELETE
#define ENABLE_FANCY_PLAYLIST_MANAGMENT_CMD_MOVE
#define ENABLE_SONG_ID
#define ENABLE_PLCHANGES
#define MPD_ERROR(c) (c==NULL || c->connection==NULL || c->connection->error)
/* Error callbacks */
static gint
error_cb(mpdclient_t *c, gint error, gchar *msg)
{
GList *list = c->error_callbacks;
if( list==NULL )
fprintf(stderr, "error [%d]: %s\n", (error & 0xFF), msg);
while(list)
{
mpdc_error_cb_t cb = list->data;
if( cb )
cb(c, error, msg);
list=list->next;
}
mpd_clearError(c->connection);
return error;
}
#ifdef DEBUG
#include "strfsong.h"
static gchar *
get_song_name(mpd_Song *song)
{
static gchar name[256];
strfsong(name, 256, "[%artist% - ]%title%|%file%", song);
return name;
}
#endif
/****************************************************************************/
/*** mpdclient functions ****************************************************/
/****************************************************************************/
gint
mpdclient_finish_command(mpdclient_t *c)
{
mpd_finishCommand(c->connection);
if( c->connection->error )
{
gchar *msg = locale_to_utf8(c->connection->errorStr);
gint error = c->connection->error;
if( error == MPD_ERROR_ACK )
error = error | (c->connection->errorCode << 8);
error_cb(c, error, msg);
g_free(msg);
return error;
}
return 0;
}
mpdclient_t *
mpdclient_new(void)
{
mpdclient_t *c;
c = g_malloc0(sizeof(mpdclient_t));
return c;
}
mpdclient_t *
mpdclient_free(mpdclient_t *c)
{
mpdclient_disconnect(c);
g_list_free(c->error_callbacks);
g_list_free(c->playlist_callbacks);
g_list_free(c->browse_callbacks);
g_free(c);
return NULL;
}
gint
mpdclient_disconnect(mpdclient_t *c)
{
if( c->connection )
mpd_closeConnection(c->connection);
c->connection = NULL;
if( c->status )
mpd_freeStatus(c->status);
c->status = NULL;
if( c->playlist.list )
mpdclient_playlist_free(&c->playlist);
if( c->song )
c->song = NULL;
return 0;
}
gint
mpdclient_connect(mpdclient_t *c,
gchar *host,
gint port,
gfloat timeout,
gchar *password)
{
gint retval = 0;
/* close any open connection */
if( c->connection )
mpdclient_disconnect(c);
/* connect to MPD */
c->connection = mpd_newConnection(host, port, timeout);
if( c->connection->error )
return error_cb(c, c->connection->error, c->connection->errorStr);
/* send password */
if( password )
{
mpd_sendPasswordCommand(c->connection, password);
retval = mpdclient_finish_command(c);
}
c->need_update = TRUE;
return retval;
}
gint
mpdclient_update(mpdclient_t *c)
{
gint retval = 0;
if( MPD_ERROR(c) )
return -1;
/* free the old status */
if( c->status )
mpd_freeStatus(c->status);
/* retreive new status */
mpd_sendStatusCommand(c->connection);
c->status = mpd_getStatus(c->connection);
if( (retval=mpdclient_finish_command(c)) )
return retval;
#ifdef DEBUG
if( c->status->error )
D("status> %s\n", c->status->error);
#endif
/* check if the playlist needs an update */
if( c->playlist.id != c->status->playlist )
{
if( c->playlist.list )
retval = mpdclient_playlist_update_changes(c);
else
retval = mpdclient_playlist_update(c);
}
/* update the current song */
if( !c->song || c->status->songid != c->song->id )
{
c->song = playlist_get_song(c, c->status->song);
}
c->need_update = FALSE;
return retval;
}
/****************************************************************************/
/*** MPD Commands **********************************************************/
/****************************************************************************/
gint
mpdclient_cmd_play(mpdclient_t *c, gint index)
{
#ifdef ENABLE_SONG_ID
mpd_Song *song = playlist_get_song(c, index);
D("Play id:%d\n", song ? song->id : -1);
if( song )
mpd_sendPlayIdCommand(c->connection, song->id);
else
mpd_sendPlayIdCommand(c->connection, MPD_PLAY_AT_BEGINNING);
#else
mpd_sendPlayCommand(c->connection, index);
#endif
c->need_update = TRUE;
return mpdclient_finish_command(c);
}
gint
mpdclient_cmd_pause(mpdclient_t *c, gint value)
{
mpd_sendPauseCommand(c->connection, value);
return mpdclient_finish_command(c);
}
gint
mpdclient_cmd_stop(mpdclient_t *c)
{
mpd_sendStopCommand(c->connection);
return mpdclient_finish_command(c);
}
gint
mpdclient_cmd_next(mpdclient_t *c)
{
mpd_sendNextCommand(c->connection);
c->need_update = TRUE;
return mpdclient_finish_command(c);
}
gint
mpdclient_cmd_prev(mpdclient_t *c)
{
mpd_sendPrevCommand(c->connection);
c->need_update = TRUE;
return mpdclient_finish_command(c);
}
gint
mpdclient_cmd_seek(mpdclient_t *c, gint id, gint pos)
{
D("Seek id:%d\n", id);
mpd_sendSeekIdCommand(c->connection, id, pos);
return mpdclient_finish_command(c);
}
gint
mpdclient_cmd_shuffle(mpdclient_t *c)
{
mpd_sendShuffleCommand(c->connection);
c->need_update = TRUE;
return mpdclient_finish_command(c);
}
gint
mpdclient_cmd_clear(mpdclient_t *c)
{
gint retval = 0;
mpd_sendClearCommand(c->connection);
retval = mpdclient_finish_command(c);
/* call playlist updated callback */
mpdclient_playlist_callback(c, PLAYLIST_EVENT_CLEAR, NULL);
c->need_update = TRUE;
return retval;
}
gint
mpdclient_cmd_repeat(mpdclient_t *c, gint value)
{
mpd_sendRepeatCommand(c->connection, value);
return mpdclient_finish_command(c);
}
gint
mpdclient_cmd_random(mpdclient_t *c, gint value)
{
mpd_sendRandomCommand(c->connection, value);
return mpdclient_finish_command(c);
}
gint
mpdclient_cmd_crossfade(mpdclient_t *c, gint value)
{
mpd_sendCrossfadeCommand(c->connection, value);
return mpdclient_finish_command(c);
}
gint
mpdclient_cmd_db_update_utf8(mpdclient_t *c, gchar *path)
{
mpd_sendUpdateCommand(c->connection, path ? path : "");
return mpdclient_finish_command(c);
}
gint
mpdclient_cmd_volume(mpdclient_t *c, gint value)
{
mpd_sendSetvolCommand(c->connection, value);
return mpdclient_finish_command(c);
}
gint
mpdclient_cmd_add_path_utf8(mpdclient_t *c, gchar *path_utf8)
{
mpd_sendAddCommand(c->connection, path_utf8);
return mpdclient_finish_command(c);
}
gint
mpdclient_cmd_add_path(mpdclient_t *c, gchar *path)
{
gint retval;
gchar *path_utf8 = locale_to_utf8(path);
retval=mpdclient_cmd_add_path_utf8(c, path_utf8);
g_free(path_utf8);
return retval;
}
gint
mpdclient_cmd_add(mpdclient_t *c, mpd_Song *song)
{
gint retval = 0;
if( !song || !song->file )
return -1;
/* send the add command to mpd */
mpd_sendAddCommand(c->connection, song->file);
if( (retval=mpdclient_finish_command(c)) )
return retval;
#ifdef ENABLE_FANCY_PLAYLIST_MANAGMENT_CMD_ADD
/* add the song to playlist */
c->playlist.list = g_list_append(c->playlist.list, mpd_songDup(song));
c->playlist.length++;
/* increment the playlist id, so we dont retrives a new playlist */
c->playlist.id++;
/* call playlist updated callback */
mpdclient_playlist_callback(c, PLAYLIST_EVENT_ADD, (gpointer) song);
#else
c->need_update = TRUE;
#endif
return 0;
}
gint
mpdclient_cmd_delete(mpdclient_t *c, gint index)
{
gint retval = 0;
mpd_Song *song = playlist_get_song(c, index);
if( !song )
return -1;
/* send the delete command to mpd */
#ifdef ENABLE_SONG_ID
D("Delete id:%d\n", song->id);
mpd_sendDeleteIdCommand(c->connection, song->id);
#else
mpd_sendDeleteCommand(c->connection, index);
#endif
if( (retval=mpdclient_finish_command(c)) )
return retval;
#ifdef ENABLE_FANCY_PLAYLIST_MANAGMENT_CMD_DELETE
/* increment the playlist id, so we dont retrive a new playlist */
c->playlist.id++;
/* remove the song from the playlist */
c->playlist.list = g_list_remove(c->playlist.list, (gpointer) song);
c->playlist.length = g_list_length(c->playlist.list);
/* call playlist updated callback */
mpdclient_playlist_callback(c, PLAYLIST_EVENT_DELETE, (gpointer) song);
/* remove references to the song */
if( c->song == song )
{
c->song = NULL;
c->need_update = TRUE;
}
/* free song */
mpd_freeSong(song);
#else
c->need_update = TRUE;
#endif
return 0;
}
gint
mpdclient_cmd_move(mpdclient_t *c, gint old_index, gint new_index)
{
gint n, index1, index2;
GList *item1, *item2;
gpointer data1, data2;
mpd_Song *song1, *song2;
if( old_index==new_index || new_index<0 || new_index>=c->playlist.length )
return -1;
song1 = playlist_get_song(c, old_index);
song2 = playlist_get_song(c, new_index);
/* send the move command to mpd */
#ifdef ENABLE_SONG_ID
D("Swaping id:%d with id:%d\n", song1->id, song2->id);
mpd_sendSwapIdCommand(c->connection, song1->id, song2->id);
#else
D("Moving index %d to id:%d\n", old_index, new_index);
mpd_sendMoveCommand(c->connection, old_index, new_index);
#endif
if( (n=mpdclient_finish_command(c)) )
return n;
#ifdef ENABLE_FANCY_PLAYLIST_MANAGMENT_CMD_MOVE
/* update the songs position field */
n = song1->pos;
song1->pos = song2->pos;
song2->pos = n;
index1 = MIN(old_index, new_index);
index2 = MAX(old_index, new_index);
/* retreive the list items */
item1 = g_list_nth(c->playlist.list, index1);
item2 = g_list_nth(c->playlist.list, index2);
/* retrieve the songs */
data1 = item1->data;
data2 = item2->data;
/* move the second item */
c->playlist.list = g_list_remove(c->playlist.list, data2);
c->playlist.list = g_list_insert_before(c->playlist.list, item1, data2);
/* move the first item */
if( index2-index1 >1 )
{
item2 = g_list_nth(c->playlist.list, index2);
c->playlist.list = g_list_remove(c->playlist.list, data1);
c->playlist.list = g_list_insert_before(c->playlist.list, item2, data1);
}
/* increment the playlist id, so we dont retrives a new playlist */
c->playlist.id++;
#else
c->need_update = TRUE;
#endif
/* call playlist updated callback */
mpdclient_playlist_callback(c, PLAYLIST_EVENT_MOVE, (gpointer) &new_index);
return 0;
}
gint
mpdclient_cmd_save_playlist_utf8(mpdclient_t *c, gchar *filename_utf8)
{
gint retval = 0;
mpd_sendSaveCommand(c->connection, filename_utf8);
if( (retval=mpdclient_finish_command(c)) == 0 )
mpdclient_browse_callback(c, BROWSE_PLAYLIST_SAVED, NULL);
return retval;
}
gint
mpdclient_cmd_save_playlist(mpdclient_t *c, gchar *filename)
{
gint retval = 0;
gchar *filename_utf8 = locale_to_utf8(filename);
retval = mpdclient_cmd_save_playlist_utf8(c, filename);
g_free(filename_utf8);
return retval;
}
gint
mpdclient_cmd_load_playlist_utf8(mpdclient_t *c, gchar *filename_utf8)
{
mpd_sendLoadCommand(c->connection, filename_utf8);
c->need_update = TRUE;
return mpdclient_finish_command(c);
}
gint
mpdclient_cmd_delete_playlist_utf8(mpdclient_t *c, gchar *filename_utf8)
{
gint retval = 0;
mpd_sendRmCommand(c->connection, filename_utf8);
if( (retval=mpdclient_finish_command(c)) == 0 )
mpdclient_browse_callback(c, BROWSE_PLAYLIST_DELETED, NULL);
return retval;
}
gint
mpdclient_cmd_delete_playlist(mpdclient_t *c, gchar *filename)
{
gint retval = 0;
gchar *filename_utf8 = locale_to_utf8(filename);
retval = mpdclient_cmd_delete_playlist_utf8(c, filename_utf8);
g_free(filename_utf8);
return retval;
}
/****************************************************************************/
/*** Callback managment functions *******************************************/
/****************************************************************************/
static void
do_list_callbacks(mpdclient_t *c, GList *list, gint event, gpointer data)
{
while(list)
{
mpdc_list_cb_t fn = list->data;
fn(c, event, data);
list=list->next;
}
}
void
mpdclient_playlist_callback(mpdclient_t *c, int event, gpointer data)
{
do_list_callbacks(c, c->playlist_callbacks, event, data);
}
void
mpdclient_install_playlist_callback(mpdclient_t *c,mpdc_list_cb_t cb)
{
c->playlist_callbacks = g_list_append(c->playlist_callbacks, cb);
}
void
mpdclient_remove_playlist_callback(mpdclient_t *c, mpdc_list_cb_t cb)
{
c->playlist_callbacks = g_list_remove(c->playlist_callbacks, cb);
}
void
mpdclient_browse_callback(mpdclient_t *c, int event, gpointer data)
{
do_list_callbacks(c, c->browse_callbacks, event, data);
}
void
mpdclient_install_browse_callback(mpdclient_t *c,mpdc_list_cb_t cb)
{
c->browse_callbacks = g_list_append(c->browse_callbacks, cb);
}
void
mpdclient_remove_browse_callback(mpdclient_t *c, mpdc_list_cb_t cb)
{
c->browse_callbacks = g_list_remove(c->browse_callbacks, cb);
}
void
mpdclient_install_error_callback(mpdclient_t *c, mpdc_error_cb_t cb)
{
c->error_callbacks = g_list_append(c->error_callbacks, cb);
}
void
mpdclient_remove_error_callback(mpdclient_t *c, mpdc_error_cb_t cb)
{
c->error_callbacks = g_list_remove(c->error_callbacks, cb);
}
/****************************************************************************/
/*** Playlist managment functions *******************************************/
/****************************************************************************/
gint
mpdclient_playlist_free(mpdclient_playlist_t *playlist)
{
GList *list = g_list_first(playlist->list);
while(list)
{
mpd_Song *song = (mpd_Song *) list->data;
mpd_freeSong(song);
list=list->next;
}
g_list_free(playlist->list);
memset(playlist, 0, sizeof(mpdclient_playlist_t));
return 0;
}
/* update playlist */
gint
mpdclient_playlist_update(mpdclient_t *c)
{
mpd_InfoEntity *entity;
D("mpdclient_playlist_update() [%lld]\n", c->status->playlist);
if( MPD_ERROR(c) )
return -1;
if( c->playlist.list )
mpdclient_playlist_free(&c->playlist);
mpd_sendPlaylistInfoCommand(c->connection,-1);
while( (entity=mpd_getNextInfoEntity(c->connection)) )
{
if(entity->type==MPD_INFO_ENTITY_TYPE_SONG)
{
mpd_Song *song = mpd_songDup(entity->info.song);
c->playlist.list = g_list_append(c->playlist.list, (gpointer) song);
c->playlist.length++;
}
mpd_freeInfoEntity(entity);
}
c->playlist.id = c->status->playlist;
c->song = NULL;
c->playlist.updated = TRUE;
/* call playlist updated callbacks */
mpdclient_playlist_callback(c, PLAYLIST_EVENT_UPDATED, NULL);
return mpdclient_finish_command(c);
}
#ifdef ENABLE_PLCHANGES
gint
mpdclient_compare_songs(gconstpointer a, gconstpointer b)
{
mpd_Song *song1 = (mpd_Song *) a;
mpd_Song *song2 = (mpd_Song *) b;
return song1->pos - song2->pos;
}
/* update playlist (plchanges) */
gint
mpdclient_playlist_update_changes(mpdclient_t *c)
{
mpd_InfoEntity *entity;
D("mpdclient_playlist_update_changes() [%lld -> %lld]\n",
c->status->playlist, c->playlist.id);
if( MPD_ERROR(c) )
return -1;
mpd_sendPlChangesCommand(c->connection, c->playlist.id);
while( (entity=mpd_getNextInfoEntity(c->connection)) != NULL )
{
if(entity->type==MPD_INFO_ENTITY_TYPE_SONG)
{
mpd_Song *song = entity->info.song;
GList *item;
item = playlist_lookup(c, song->id);
#ifdef DEBUG
if( item )
D("Changing index:%d, pos:%d, id:%d => %s\n",
g_list_position(c->playlist.list, item),
song->pos, song->id, get_song_name(song));
else
D("Unable to find pos:%d, id:%d => %s\n",
song->pos, song->id, get_song_name(song));
#endif
if( item && item->data)
{
GList *old;
gint index = g_list_position(c->playlist.list, item);
/* remove previous song at song->pos */
while( song->pos != (index=g_list_position(c->playlist.list, item))
&&
(old=g_list_nth(c->playlist.list, song->pos)) )
{
D("Removing item with index %d id:%d (%d)\n",
song->pos, old ? ((mpd_Song *) old->data)->id : -1, index);
if( item->data == c->song )
c->song = NULL;
mpd_freeSong((mpd_Song *) old->data);
old->data = NULL;
c->playlist.list = g_list_delete_link(c->playlist.list, old);
c->playlist.length = g_list_length(c->playlist.list);
}
/* Update playlist entry */
mpd_freeSong((mpd_Song *) item->data);
item->data = mpd_songDup(song);
if( c->song && c->song->id == song->id )
c->song = item->data;
}
else
{
/* Add a new playlist entry */
D("Adding pos:%d, id;%d - %s\n",
song->pos, song->id, get_song_name(song));
c->playlist.list = g_list_append(c->playlist.list,
(gpointer) mpd_songDup(song));
c->playlist.length++;
}
}
mpd_freeInfoEntity(entity);
}
mpd_finishCommand(c->connection);
while( g_list_length(c->playlist.list) > c->status->playlistLength )
{
GList *item = g_list_last(c->playlist.list);
/* Remove the last playlist entry */
mpd_freeSong((mpd_Song *) item->data);
c->playlist.list = g_list_delete_link(c->playlist.list, item);
c->playlist.length--;
D("Removed the last playlist entry\n");
}
c->playlist.id = c->status->playlist;
c->playlist.updated = TRUE;
mpdclient_playlist_callback(c, PLAYLIST_EVENT_UPDATED, NULL);
return 0;
}
#else
gint
mpdclient_playlist_update_changes(mpdclient_t *c)
{
return mpdclient_playlist_update(c);
}
#endif
mpd_Song *
playlist_get_song(mpdclient_t *c, gint index)
{
return (mpd_Song *) g_list_nth_data(c->playlist.list, index);
}
GList *
playlist_lookup(mpdclient_t *c, int id)
{
GList *list = g_list_first(c->playlist.list);
while( list )
{
mpd_Song *song = (mpd_Song *) list->data;
if( song->id == id )
return list;
list=list->next;
}
return NULL;
}
mpd_Song *
playlist_lookup_song(mpdclient_t *c, gint id)
{
GList *list = c->playlist.list;
while( list )
{
mpd_Song *song = (mpd_Song *) list->data;
if( song->id == id )
return song;
list=list->next;
}
return NULL;
}
gint
playlist_get_index(mpdclient_t *c, mpd_Song *song)
{
return g_list_index(c->playlist.list, song);
}
gint
playlist_get_index_from_id(mpdclient_t *c, gint id)
{
return g_list_index(c->playlist.list, playlist_lookup_song(c, id));
}
gint
playlist_get_index_from_file(mpdclient_t *c, gchar *filename)
{
GList *list = c->playlist.list;
gint i=0;
while( list )
{
mpd_Song *song = (mpd_Song *) list->data;
if( strcmp(song->file, filename ) == 0 )
return i;
list=list->next;
i++;
}
return -1;
}
/****************************************************************************/
/*** Filelist functions *****************************************************/
/****************************************************************************/
mpdclient_filelist_t *
mpdclient_filelist_free(mpdclient_filelist_t *filelist)
{
GList *list = g_list_first(filelist->list);
D("mpdclient_filelist_free()\n");
while( list!=NULL )
{
filelist_entry_t *entry = list->data;
if( entry->entity )
mpd_freeInfoEntity(entry->entity);
g_free(entry);
list=list->next;
}
g_list_free(filelist->list);
g_free(filelist->path);
filelist->path = NULL;
filelist->list = NULL;
filelist->length = 0;
g_free(filelist);
return NULL;
}
mpdclient_filelist_t *
mpdclient_filelist_get(mpdclient_t *c, gchar *path)
{
mpdclient_filelist_t *filelist;
mpd_InfoEntity *entity;
gchar *path_utf8 = locale_to_utf8(path);
D("mpdclient_filelist_get(%s)\n", path);
mpd_sendLsInfoCommand(c->connection, path_utf8);
filelist = g_malloc0(sizeof(mpdclient_filelist_t));
if( path && path[0] && strcmp(path, "/") )
{
/* add a dummy entry for ./.. */
filelist_entry_t *entry = g_malloc0(sizeof(filelist_entry_t));
entry->entity = NULL;
filelist->list = g_list_append(filelist->list, (gpointer) entry);
filelist->length++;
}
while( (entity=mpd_getNextInfoEntity(c->connection)) )
{
filelist_entry_t *entry = g_malloc0(sizeof(filelist_entry_t));
entry->entity = entity;
filelist->list = g_list_append(filelist->list, (gpointer) entry);
filelist->length++;
}
if( mpdclient_finish_command(c) )
{
g_free(path_utf8);
return mpdclient_filelist_free(filelist);
}
g_free(path_utf8);
filelist->path = g_strdup(path);
filelist->updated = TRUE;
return filelist;
}
mpdclient_filelist_t *
mpdclient_filelist_update(mpdclient_t *c, mpdclient_filelist_t *filelist)
{
if( filelist != NULL )
{
gchar *path = g_strdup(filelist->path);
filelist = mpdclient_filelist_free(filelist);
filelist = mpdclient_filelist_get(c, path);
g_free(path);
return filelist;
}
return NULL;
}
filelist_entry_t *
mpdclient_filelist_find_song(mpdclient_filelist_t *fl, mpd_Song *song)
{
GList *list = g_list_first(fl->list);
while( list && song)
{
filelist_entry_t *entry = list->data;
mpd_InfoEntity *entity = entry->entity;
if( entity && entity->type==MPD_INFO_ENTITY_TYPE_SONG )
{
mpd_Song *song2 = entity->info.song;
if( strcmp(song->file, song2->file) == 0 )
{
return entry;
}
}
list = list->next;
}
return NULL;
}