/* MusicInfo plugin for pidgin Displays whatever is currently playing in Winamp in the user's profile, away message, or available message in Pidgin Written by Reuben Balik Copyright 2005, 2006, 2007, 2008 By Reuben Balik Please ask permission before using any of this code for any other project/product. This code/plugin is provided as-is with no warranty of any kind. Use at your own risk. Special thanks to Leonardo Fernandes for some of the code. Last Update: April 23, 2008 */ #include "musicinfo.h" #include "internal.h" #include "status.h" #include "connection.h" #include "signals.h" #include "version.h" #include "notify.h" #include "savedstatuses.h" #include "debug.h" #include #include //these are all the strings that the plugin searches for static char artist_replace[] = "%ar%"; static char title_replace[] = "%so%"; static char start_songText[] = "(mi)"; static char end_songText[] = "(/mi)"; static char start_noSongText[] = "(ns)"; static char end_noSongText[] = "(/ns)"; //window classes static char winamp_wnd[] = "Winamp v1.x"; static int timeoutint = 0; static GList* accounts = NULL; static gboolean under_recursion = TRUE; static gboolean privateMode = FALSE; static const char pluginTitle[] = "MusicInfo"; static const char pModeOn[] = "MusicInfo Privacy Mode is now ON"; static const char pModeOn_sub[] = "Your buddies will not be able to see what you are listening to."; static const char pModeOff[] = "MusicInfo Privacy Mode is now OFF"; static const char pModeOff_sub[] = "Your buddies will be able to see what you are listening to."; //Struct to hold account variables typedef struct { char* prevInfo; PurpleConnection* c; } AcctVar; // getSong gets the name of the song and artist from the title of the winamp window // and puts them in the buffers pointed to by title and artist // len is the size of the title and artist buffers in bytes // getSong returns FALSE if Winamp/foobar is closed or not playing, TRUE if it successfully got a song static gboolean getSong(char* title, char* artist, int len) { gsize written; char this_title[len*3], temp[len]; int ret, count; HWND hwndPlayer; gchar* strinutf; char *firsthyphen, *secondhyphen, *ptr; if(privateMode) return FALSE; //get handle to winamp window hwndPlayer = FindWindow(winamp_wnd, NULL); if(hwndPlayer == NULL) return FALSE; ret = GetWindowText(hwndPlayer,this_title,sizeof(this_title)); if(ret == 0) return FALSE; if(strstr(this_title, "[Stopped]") != NULL) return FALSE; if(strstr(this_title, "[Paused]") != NULL) return FALSE; firsthyphen = g_strstr_len(this_title, len, " - "); if(firsthyphen == NULL) //when winamp starts up without playing it doesn't display the song name return FALSE; secondhyphen = g_strrstr_len(this_title, len, " - "); ptr = g_strstr_len(this_title, len, ".") + 2; count = 0; if(firsthyphen != secondhyphen) //we have an artist { while(ptr != firsthyphen && count < len) { temp[count] = *ptr; ptr++; count++; } temp[count] = '\0'; ptr+=3; if(!g_utf8_validate(temp, -1, NULL)) { if( NULL == (strinutf = g_locale_to_utf8(temp, -1, NULL, &written, NULL))) return FALSE; //it couldn't convert for some reason g_strlcpy(artist, strinutf, len); g_free(strinutf); } else { g_strlcpy(artist, temp, len); } } else //no artist { strcpy(artist, "Unknown"); } count=0; while(ptr != secondhyphen && count < len) { temp[count] = *ptr; ptr++; count++; } temp[count] = '\0'; if(!g_utf8_validate(temp, -1, NULL)) { if( NULL == (strinutf = g_locale_to_utf8(temp, -1, NULL, &written, NULL))) return FALSE; //it couldn't convert for some reason g_strlcpy(title, strinutf, len); g_free(strinutf); } else { g_strlcpy(title, temp, len); } return TRUE; } // replaces the first instance of replaceStr in text with replaceWith // max is the size of text // all strings should already be in utf8 but i'm not actually using the utf8 functions // because this is easier just going by bytes instead of characters static gboolean replaceText(char* text, char* replaceStr, char* replaceWith, int max) { char *pos, *foundAt, *replacePos; int cur; char newString[max]; foundAt = strstr(text, replaceStr); if(foundAt == NULL) return FALSE; pos = text; cur = 0; while(pos < foundAt && cur < max) { newString[cur] = *pos; pos++; cur++; } replacePos = replaceWith; while(*replacePos != '\0' && cur < max) { newString[cur] = *replacePos; replacePos++; cur++; } pos+=strlen(replaceStr); while(*pos != '\0' && cur < max) { newString[cur] = *pos; pos++; cur++; } newString[cur] = '\0'; g_strlcpy(text, newString, max); return TRUE; } //hides all text from startReplace to endReplace with a max string size of len static void hideBetween(char* text, char* startReplace, char* endReplace, unsigned int len) { char *hidepos, *endhidepos, *pos; char newString[len]; int n; pos = text; hidepos = strstr(text, startReplace); endhidepos = strstr(text, endReplace) + strlen(endReplace); if(hidepos != NULL && endhidepos > hidepos) { n = 0; while(pos < hidepos && n < len) { newString[n] = *pos; n++; pos++; } pos = endhidepos; while(*pos != '\0' && n < len) { newString[n] = *pos; n++; pos++; } newString[n] = '\0'; g_strlcpy(text, newString, len); } } //gets the text of whatever the user sets the status to. (not necessarily what status is on the server) static gboolean getStatusText(char* client_text, char* server_text, PurpleAccount *account, const char** sid, int len) { PurpleStatus* status; PurpleSavedStatus* savedStatus; const char* statusMessage; status = purple_account_get_active_status(account); if (status == NULL) { purple_debug_fatal("MusicInfo", "Account with NULL status\n"); return FALSE; } *sid = purple_status_get_id(status); statusMessage = purple_status_get_attr_string(status, "message"); if(statusMessage != NULL) g_strlcpy(server_text, statusMessage, len); else *server_text = '\0'; savedStatus = purple_savedstatus_get_current(); if (savedStatus == NULL) { purple_debug_fatal("MusicInfo", "Account with NULL saved status\n"); return FALSE; } statusMessage = purple_savedstatus_get_message(savedStatus); if(statusMessage != NULL) g_strlcpy(client_text, statusMessage, len); else return FALSE; return TRUE; } // update the info for the connection gc // prevInfo and prevStat are the previously set data for that account (so we don't keep setting the same thing over) static void updateInfo(PurpleConnection *gc, char* prevInfo, char* title, char* artist, gboolean isPlaying, gboolean updateStatus) { PurpleAccount* account; char curAcct[MAX_PROFILE_SIZE]; char statusText[MAX_PROFILE_SIZE]; char servStatusText[MAX_PROFILE_SIZE]; const char* sid = NULL; const char* acctInfo = NULL; gboolean hasStatus = FALSE; account = purple_connection_get_account(gc); acctInfo = purple_account_get_user_info(account); if(acctInfo != NULL) g_strlcpy(curAcct, acctInfo, MAX_PROFILE_SIZE); else curAcct[0] = '\0'; hasStatus = getStatusText(statusText, servStatusText, account, &sid, MAX_PROFILE_SIZE); if(isPlaying) { //replace the appropriate text replaceText(curAcct, start_songText, "\0", MAX_PROFILE_SIZE); replaceText(curAcct, end_songText, "\0", MAX_PROFILE_SIZE); replaceText(curAcct, artist_replace, artist, MAX_PROFILE_SIZE); replaceText(curAcct, title_replace, title, MAX_PROFILE_SIZE); //hide everything in the "no song" tag hideBetween(curAcct, start_noSongText, end_noSongText, MAX_PROFILE_SIZE); //and do it for the status if(hasStatus) { replaceText(statusText, start_songText, "\0", MAX_PROFILE_SIZE); replaceText(statusText, end_songText, "\0", MAX_PROFILE_SIZE); replaceText(statusText, artist_replace, artist, MAX_PROFILE_SIZE); replaceText(statusText, title_replace, title, MAX_PROFILE_SIZE); hideBetween(statusText, start_noSongText, end_noSongText, MAX_PROFILE_SIZE); } } else { //replace the appropriate text replaceText(curAcct, start_noSongText, "\0", MAX_PROFILE_SIZE); replaceText(curAcct, end_noSongText, "\0", MAX_PROFILE_SIZE); //hide everything in the song tag hideBetween(curAcct, start_songText, end_songText, MAX_PROFILE_SIZE); //and for the status: if(hasStatus) { replaceText(statusText, start_noSongText, "\0", MAX_PROFILE_SIZE); replaceText(statusText, end_noSongText, "\0", MAX_PROFILE_SIZE); hideBetween(statusText, start_songText, end_songText, MAX_PROFILE_SIZE); } } //this is so we don't update the server if we don't have to if(strcmp(curAcct, prevInfo) != 0) { serv_set_info(gc, curAcct); g_strlcpy(prevInfo, curAcct, MAX_PROFILE_SIZE); } if(hasStatus && (strcmp(statusText, servStatusText) != 0 || updateStatus)) { under_recursion = TRUE; purple_account_set_status(account, sid, TRUE, "message", statusText, NULL); under_recursion = FALSE; } } // this is the function called by the timer // it loops through all accounts static gboolean callUpdateInfo() { GList* cur; AcctVar* curAcct; gboolean isPlaying; char title[MAX_NAME_SIZE]; char artist[MAX_NAME_SIZE]; isPlaying = getSong(title, artist, MAX_NAME_SIZE); cur = g_list_first(accounts); while(cur != NULL) { curAcct = (AcctVar*)cur->data; updateInfo(curAcct->c, curAcct->prevInfo, title, artist, isPlaying, FALSE); cur = cur->next; } return TRUE; } static void toggle_privacy(PurplePluginAction *action) { privateMode = !privateMode; if(privateMode) purple_notify_info(NULL, pluginTitle, pModeOn, pModeOn_sub); else purple_notify_info(NULL, pluginTitle, pModeOff, pModeOff_sub); } static void online(PurpleConnection *gc) { char* prevInfo; AcctVar* acctvar; prevInfo = (char*) g_malloc(sizeof(char) * MAX_PROFILE_SIZE); acctvar = (AcctVar*) g_malloc(sizeof(AcctVar)); *prevInfo = '\0'; acctvar->prevInfo = prevInfo; acctvar->c = gc; accounts = g_list_append(accounts, (gpointer)acctvar); if(timeoutint == 0) { timeoutint = g_timeout_add(CHECK_INTERVAL, callUpdateInfo, NULL); } } static void status_changed(PurpleAccount *account, PurpleStatus *old, PurpleStatus *news) { char title[MAX_NAME_SIZE]; char artist[MAX_NAME_SIZE]; char* prevInfo; gboolean isPlaying, flag; GList* cur; AcctVar* curAcct; PurpleConnection* pc; if (under_recursion) return; pc = purple_account_get_connection(account); isPlaying = getSong(title, artist, MAX_NAME_SIZE); cur = g_list_first(accounts); prevInfo = NULL; curAcct = NULL; flag = FALSE; //find the correct account so we can get the prevInfo while(cur != NULL && !flag) { curAcct = (AcctVar*)cur->data; if(curAcct->c == pc) flag = TRUE; cur = cur->next; } if(flag) updateInfo(pc, curAcct->prevInfo, title, artist, isPlaying, TRUE); } static void signoff(PurpleConnection *gc) { guint len = g_list_length(accounts); GList* cur; AcctVar* curAcct; gboolean flag = FALSE; cur = g_list_first(accounts); while(cur != NULL && !flag) { curAcct = (AcctVar*)cur->data; if( curAcct->c == gc) { flag = TRUE; g_free(curAcct->prevInfo); g_free(curAcct); g_list_delete_link(accounts, cur); } cur = cur->next; } if(len == 1) { g_source_remove(timeoutint); timeoutint = 0; } } static gboolean plugin_load(PurplePlugin *plugin) { purple_signal_connect(purple_connections_get_handle(), "signed-on", plugin, PURPLE_CALLBACK(online), NULL); purple_signal_connect(purple_connections_get_handle(), "signing-off", plugin, PURPLE_CALLBACK(signoff), NULL); purple_signal_connect(purple_accounts_get_handle(), "account-status-changed", plugin, PURPLE_CALLBACK(status_changed), NULL); under_recursion = TRUE; return TRUE; } static gboolean plugin_unload(PurplePlugin *plugin) { purple_signal_disconnect(purple_connections_get_handle(), "signed-on", plugin, PURPLE_CALLBACK(online)); purple_signal_disconnect(purple_connections_get_handle(), "signing-off", plugin, PURPLE_CALLBACK(signoff)); purple_signal_disconnect(purple_accounts_get_handle(), "account-status-changed", plugin, PURPLE_CALLBACK(status_changed)); if(timeoutint > 0) { g_source_remove(timeoutint); timeoutint = 0; } return TRUE; } static GList * miactions(PurplePlugin *plugin, gpointer context) { GList *actlist = NULL; PurplePluginAction *act = NULL; act = purple_plugin_action_new(_("Toggle Privacy Mode"), toggle_privacy); actlist = g_list_append(actlist, act); return actlist; } static PurplePluginInfo info = { PURPLE_PLUGIN_MAGIC, PURPLE_MAJOR_VERSION, PURPLE_MINOR_VERSION, PURPLE_PLUGIN_STANDARD, /**< type */ NULL, /**< ui_requirement */ 0, /**< flags */ NULL, /**< dependencies */ PURPLE_PRIORITY_DEFAULT, /**< priority */ "gtk-win32-rbalik-musicinfo", /**< id */ "MusicInfo", /**< name */ "1.1", /**< version */ /** summary */ "Share your musical taste with people on your buddy list", /** description */ "Displays whatever song you are listening to in your info, away message, or available message.", "Reuben Balik ", /**< author */ "http://mi.reubenbalik.com", /**< homepage */ plugin_load, /**< load */ plugin_unload, /**< unload */ NULL, /**< destroy */ NULL, /**< ui_info */ NULL, /**< extra_info */ NULL, /**< prefs_info */ miactions, //seems that pidgin added a few extra entries to this struct NULL, NULL, NULL, NULL, }; static void init_plugin(PurplePlugin *plugin) { } PURPLE_INIT_PLUGIN(musicinfo, init_plugin, info)